ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 4-3. 클래스와 객체 다루기 : 단순한 변수 이상의 프로퍼티
    Study(종료)/Kotlin 22.09.13 ~ 12.18 2022. 10. 1. 00:18

    4장

    4-1. 클래스와 객체 다루기 : 클래스 정의하기

    4-2. 클래스와 객체 다루기 : 널 가능성

    4-3. 클래스와 객체 다루기 : 단순한 변수 이상의 프로퍼티

    4-4. 클래스와 객체 다루기 : 객체


    4-3. 단순한 변수 이상의 프로퍼티

    1) 최상위 프로퍼티

    클래스나 함수와 마찬가지로 최상위 수준에 프로퍼티를 정의할 수 있다.

    이 경우 프로퍼티는 전역 변수나 상수와 비슷한 역할을 한다.

    val prefix = "hello"
    
    fun main() {
        val name = readLine() ?: return
        println("$prefix $name")
    }

    2) 늦은 초기화

    클래스를 인스턴스화할 때 프로퍼티를 초기화해야 한다는 요구 사항이 불필요하게 엄격할 때가 있다.

    어떤 프로퍼티는 클래스 인스턴스가 생성된 뒤이지만 해당 프로퍼티가 사용되는 시점보다는

    이전에 초기화돼야 할 수도 있다.

    예를 들면 단위 테스트를 준비하는 코드, 의존 관계 주입에 의해 대입되는 프로퍼티가 있다.

     

    이 경우 생성자에서는 초기화되지 않은 상태를 의미하는 디폴트 값을 대입하고 실제 필요할 때 값을 대입할 수 있다.

    import java.io.File
    
    fun main() {
        var text: String? = null
        
        fun loadFile(file: File){
            text = file.readText()
        }
    }
    
    fun getContentSize(content: Content) = content.text?.length ?: 0

    여기서 loadFile()은 다른 곳에서 호출되며 어떤 파일의 내용을 모두 문자열로 가져온다고 가정하자.

    위 코드의 단점은 실제 값이 항상 사용 전에 초기화되므로 절대 널이 될수 없는 값이지만,

    늘 널 가능성을 처리해야 한다는 점 이다.

    코틀린은 이런 문제를 해결하기 위해 lateinit 키워드를 제공한다.

    import java.io.File
    
    fun main() {
        lateinit var text: String
    
        fun loadFile(file: File){
            text = file.readText()
        }
    }
    
    fun getContentSize(content: Content) = content.text.length()

    lateinit 표시가 붙은 프로퍼티는 값을 읽으려고 시도할 때 프로그램이 프로퍼티가 초기화 여부를 검사해

    초기화되지 않은 경우 UninitializedPropertyAccessException을 던진다는 차이를 제외하면 일반 프로퍼티와 같다.

     

    프로퍼티를 lateinit으로 만들기 위해서는 만족해야 하는 조건이 이다.

    1. 프로퍼티가 코드에서 변경될 수 있는 지점이 여러 곳일 수 있으므로 가변 프로퍼티(var)로 정의해야 한다.

    2. 프로퍼티의 타입은 not-null 타입이어야 하고, Int나 Boolean 같은 원시 값을 표현하는 타입이 아니어야 한다.

    3. lateinit 프로퍼티를 정의하면서 초기화 식을 지정해 바로 대입할 수 없다(이 경우 애초에 lateinit 사용 의미가 없으므로)

    lateinit var text: String
    
    fun readText(){
        text = readLine()!!
    }
    fun main() {
        readText()
        println(text)
    }

    3) 커스텀 접근자 사용하기

    커스텀 접근자(custom accessor)를 이용하면 변수와 함수의 동작을 한 선언 안에 조합할 수 있다.

    커스텀 접근자는 프로퍼티 값을 읽거나 쓸 때 호출되는 특별한 함수다.

    class Person(val firstName: String, val lastName: String){
        val fullName: String
            get(): String{
                return "$firstName $lastName"
            }
    }

    게터는 프로퍼티 정의(위 코드에서 fullName) 끝에 붙으며

    기본적으로 이름 대신 get 이라는 키워드가 붙은 함수처럼 보인다.

    하지만 이런 프로퍼티를 읽으면 프로그램이 자동으로 게터를 호출한다.

    fun main() {
        val person = Person("김", "자반")
        println(person.fullName) //김 자반
    }

    식이 본문인 형태를 사용할 수 있으며

    val fullName: String
        get()= "$firstName $lastName"

    프로퍼티 타입을 생략하고 타입 추론에 의존해 사용할 수 있다.

    val fullName
        get()= "$firstName $lastName"

    게터에는 파라미터가 없다. 게터의 반환 타입은(만약 반환 타입을 지정한다면) 프로퍼티의 타입과 같아야 한다.

    val fullName
        //get():Int = "$firstName $lastName"
        //error : Type mismatch Required:Int Found:String
        get():String = "$firstName $lastName"

    위 코드처럼 사용하는 프로퍼티는 fullName 프로퍼티를 읽을 때 마다 다시 계산된다.

     

    이후에 뒷받침하는 필드(backing field)에 대한 내용이 있는데, 책 내용만 봐서는 이해가 안가서 공식 문서를 참고했다https://kotlinlang.org/docs/properties.html#backing-fields

     

    코틀린에서 필드는 프로퍼티의 일부로, 메모리에 프로퍼티에 값을 유지시키는 용도로만 사용된다.

    필드는 직접 선언될 수 없지만, 프로퍼티가 backing field가 필요한 경우 코틀린이 자동으로 제공한다.

    이 backing field는 식별자 field를 이용해 참조할 수 있다

    class Person(val firstName: String, val lastName: String, age: Int){
        var counter = 0 // the initializer assigns the backing field directly
            set(value) {
                if (value >= 0)
                 field = value
                 // counter = value 
                 // ERROR StackOverflow: Using actual name 'counter' would make setter recursive
            }
    }
    
    fun main() {
        val person = Person("김", "자반", 20)
        person.counter = 20
    }

    위 코드에서 counter = value를 사용하면 무한 루프에 빠지고,

    field = value를 사용하면 정상적으로 작동한다.

     

    field 식별자는 프로퍼티의 접근자(accesser)로써만 사용될 수 있다.

    만약 하나 이상의 접근자를 기본 구현하거나 field 식별자를 이용한

    커스텀 접근자로 참조할 때 backing-field는 프로퍼티가 생성된다 

    예를 들어 밑에 코드는 backing-field가 없다.

    val isEmpty: Boolean
        get() = this.size == 0

    var로 정의하는 가변 프로퍼티에는 값을 읽기 위한 getter, 값을 설정하기 위한 setter 두 가지 접근자가 있다.

    class Person(val firstName: String, val lastName: String, age: Int){
       var age: Int? = null
        set(value){
            if(value != null && value <= 0)
                throw IllegalArgumentException("Invalid age: $value")
            field = value
        }
    
    }
    
    fun main() {
        val person = Person("김", "자반", 20)
        person.age = 20 // call custom setter
        println(person.age) // call custom getter
    }

    위 코드는 자동적으로 backing-field를 생성한다.

     

    프로퍼티 세터의 파라미터는 단 하나이며 타입은 프로퍼티 자체의 타입과 같아야 한다.

    관습적으로 파라미터 이름을 value로 정하는 경우가 많지만, 다른 이름도 사용 가능하다.

    프로퍼티를 초기화하면 값을 바로 backing-field에 쓰기 때문에 프로퍼티 초기화는 setter를 호출하지 않는다.

     

    가변 프로퍼티에는 두 가지 접근자가 있으므로 두 접근자를 모두 커스텀화하고,

    두 접근자가 모두 field 키워드를 통해 backing-field를 사용하지 않는 경우를 제외하면 항상 backing-field가 생긴다.

    아래 코드는 backing-field가 생기지 않는다.

    class Person(var firstName: String, var lastName: String, age: Int){
       var fullName: String 
            get(): String = "$firstName $lastName"
            set(value){
                val names = value.split(" ")
                if(names.size != 2)
                    throw IllegalArgumentException("invalid full name : $value")
                firstName = names[0]
                lastName = names[1]
            }
    }

     프로퍼티 접근자에 가시성 변경자(visibility modifiers)를 붙일 수 있다.

    프로퍼티가 포함된 클래스 외부에서는  프로퍼티의 값을 변경 못하게 해서

    외부에서 볼 때 객체가 불변인 것 처럼 하고싶을 떄 이 방식을 사용할 수 있다.

     

    단순한 접근자 구현(backing-field를 바로 돌려주는 getter와 field에 값을 바로 대입하는 setter)만 필요한 경우

    그냥 get이나 set 키워드만 사용해 getter와 setter를 정의할 수 있다.

    import java.util.Date
    
    class Person(name: String){
       var lastChanged: Date? = null
        private set // Person 클래스 밖에서 변경 x
    
        var name: String = name
         set(value){
             lastChanged = Date()
             field = value
         }
    
    }
    
    fun main() {
        val person = Person("김자반")
        println(person.lastChanged) // null
    
        person.name = "유퀴즈"
        println(person.lastChanged) // Fri Sep 30 23:39:42 KST 2022
        // person.lastChanged = "10:11:12"
        // Error : Cannot assign to 'lastChanged': the setter is private in 'Person'
    }

    4) 지연 계산 프로퍼티와 위임

    어떤 프로퍼티를 처음 읽을 때까지 그 값에 대한 계산을 미뤄두고 싶을 때 lazy 프로퍼티를 이용하면 된다.

    import java.io.File
    
    val text by lazy {
        File("data.txt").readText()
    }
    
    fun main() {
        while(true){
            when (val command = readLine() ?: return){
                "print data" -> println(text)
                "exit" -> return
            }
        }
    }

    위 코드에서 text 프로퍼티를 lazy로 정의했다. lazy 다음에 오는 블록 안에는 프로퍼티를 초기화 하는 코드를 지정한다.

    main()함수에서 사용자가 적절한 명령으로 프로퍼티 값을 읽기 전 까지 프로그램은 lazy 프로퍼티의 값을 계산하지 않는다.

    초기화가 된 이후 프로퍼티의 값은 필드에 저장되고, 그 이후에는 값을 읽을 때 마다 저장된 값을 읽게 된다.

     

    일반적인 경우 프로그램이 시작될 떄 바로 파일을 읽는다.

    val text = File("data.txt").readText()

    앞에서 배운 getter를 사용하면, 프로퍼티 값을 읽을 때 마다 파일을 매번 다시 읽어온다.

    val text get() = File("data.txt").readText()

    필요하면 프로퍼티 타입을 명시할 수 있다.

    val text: String by lazy { File("data.txt").readText() }

    lateinit 프로퍼티와 달리 lazy 프로퍼티는 가변 프로퍼티가 아니다.

    lazy 프로퍼티는 초기화된 다음에는 변경되지 않는다.

    var text1 by lazy {"hello"}
    // error: Type 'Lazy<String>' has no method 
    // 'setValue(Nothing?, KProperty<*>, String)' and 
    // thus it cannot serve as a delegate for var (read-write property)
    
    val text2 by lazy {"hello"} // OK

    lazy 프로퍼티는 스레드 안전(thread-safe)하다.

    다중 스레드 환경에서도 값을 한 스레드 안에서만 계산하기 때문에 lazy 프로퍼티에 접근하려는

    모든 스레드는 궁극적으로 같은 값을 얻게 된다.

     

    함수 본문에서도 지연 변수를 정의할 수 있다.

    fun getName() = readLine()!!
    
    fun main() {
        val name by lazy { getName() }
        println(name)
    }

    다음글

    4-4. 클래스와 객체 다루기 : 객체

    반응형

    댓글

Designed by Tistory.