본문 바로가기

Dev Book/Kotlin IN ACTION

CH7(7.5). 프로퍼티 접근자 로직 재활용: 위임 프로퍼티

1. 위임 프로퍼티

class Foo {
    var p : Type by Delegate()
}

 

p 프로퍼티는 접근자 로직을 다른 객체에게 위임한다. Delegate 클래스의 인스턴스를 위임 객체로 사용하며 by 뒤에 있는 식을 계산해서 위임에 쓰일 객체를 얻는다. 

 

class Foo {
	private val delegate = Delegate() // 컴파일러가 생성한 도우미 프로퍼티
    var p: Type
    set(Value:Type) = delegate.setValue(..., Value)
    get() = delegate.getValue(...)
 }

 

다음과 같이 컴파일러는 숨겨진 도우미 프로퍼티를 만들고, 그 프로퍼티를 위임 객체의 인스턴스로 초기화 한다. p 프로퍼티는 바로 그 위임 객체에게 자신의 작업을 위임한다.

프로퍼티 위임 관례를 따르는 Delegate 클래스는 getValue, setValue 메소드를 제공해야 한다. 

2. 위임 프로퍼티 사용: by lazy()를 통한 프로퍼티 초기화 지연

 

class Email {/*...*/}

fun loadEmail(person:Person):List<Email>{
    println("${person.name}의 이메일을 가져옴")
    return listOf(/*...*/)
}

class Person(val name:String){
    private var _emails: List<Email>? = null
    val emails: List<Email>
        get() {
            if (_emails == null)
                _emails = loadEmail(this) //최초 접근 시 이메일을 가져온다.
            return _emails!! //저장해둔 데이터가 있으면 그 데이터를 반환
        }
}

>>>val p = Person("Alice")
>>>p.emails
Alice의 이메일을 가져옴
>>>p.emails

 

_emails라는 프로퍼티는 값을 저장하고, 다른 프로퍼티인 emails는 _emails라는 프로퍼티에 대한 읽기 연산을 제공한다. _emails는 널이 될 수 있는 타입인 반면 emails는 널이 될 수 없는 타입이므로 프로퍼티를 두 개 사용해야 한다. 이를 뒷받침하는 프로퍼티 기법이라고 한다.

 

- 위임 프로퍼티는 데이터를 저장할 때 쓰이는 뒷받침하는 프로퍼티와 값이 오직 한 번만 초기화됨을 보장하는 게터 로직을 함께 캡슐화해준다. 

 

class Person(val name:String){
    val emails by lazy { loadEmail(this) }
}

 

lazy 함수는 코틀린 관례에 맞는 시그니처의 getValue 메소드가 들어가있는 객체를 반환한다. 따라서 lazy를 by 키워드와 함께 사용해 위임 프로퍼티를 만들 수 있다.

lazy 함수의 인자는 값을 초기화할 때 호출할 람다이며 lazy 함수는 기본적으로 스레드 안전하다.

3. 위임 프로퍼티 구현

어떤 객체의 프로퍼티가 바뀔 때마다 리스너에게 변경 통지를 보내는 기능을 구현해보자.

 

1) 프로퍼티 변경 통지를 직접 구현한 경우

 

//PropertyChangeSupport를 사용하기 위한 도우미 클래스
open class PropertyChangeAware{
    protected val changeSupport = PropertyChangeSupport(this)
    fun addPropertyChangeListener(listener: PropertyChangeListener){
        changeSupport.addPropertyChangeListener(listener)
    }
    fun removePropertyChangeListener(listener: PropertyChangeListener){
        changeSupport.removePropertyChangeListener(listener)
    }
}

class Person(val name:String, age:Int, salary:Int): PropertyChangeAware(){
    var age:Int = age
        set(newValue) {
            val oldValue = field //뒷받침하는 필드에 접근 시 field 식별자 사용
            field = newValue
            changeSupport.firePropertyChange("age", oldValue, newValue) //프로퍼티 변경을 리스너에게 통지
        }

    var salary:Int = salary
        set(newValue) {
            val oldValue = field
            field = newValue
            changeSupport.firePropertyChange("salary", oldValue, newValue)
        }

}

 

>>>var person = Person("Dmitry", 34, 2000)
>>>    person.addPropertyChangeListener(
...        PropertyChangeListener { event ->
...            println("Property ${event.propertyName} changed from ${event.oldValue} to ${event.newValue}")
...        }
...    )
    
>>>person.age = 35
Property age changed from 34 to 35
>>>person.salary = 3000
Property salary changed from 2000 to 3000

 

세터 코드에 중복이 많이 존재함을 확인할 수 있다.

 

2) 도우미 클래스를 통해 프로퍼티 변경 통지를 구현한 경우

 

class ObservableProperty(
    val propName:String, var propValue:Int, val changeSupport: PropertyChangeSupport
){
    fun getValue(): Int = propValue
    fun setValue(newValue:Int){
        val oldValue = propValue
        propValue = newValue
        changeSupport.firePropertyChange(propName, oldValue, newValue)
    }
}

class Person3(val name:String, age:Int, salary:Int): PropertyChangeAware(){
    val _age = ObservableProperty("age", age, changeSupport)
    var age:Int
        get() = _age.getValue()
        set(value) {_age.setValue(value)}
    val _salary = ObservableProperty("salary", salary, changeSupport)
    var salary: Int
        get() = _salary.getValue()
        set(value){ _salary.setValue(value)}
}

 

이 코드는 코틀린의 위임이 실제로 작동하는 방식과 비슷하다. 프로퍼티 값을 저장하고 그 값이 바뀌면 자동으로 변경 통지를 전달해주는 클래스가 있고, 로직의 중복이 많이 제거되었다. 그러나 아직도 각각의 프로퍼티마다 ObservableProperty를 만들고 게터와 세터에서 ObservableProperty에 작업을 위임하는 준비 코드가 꽤 많이 필요하다. 코틀린 위임 프로퍼티 기능을 이용해 이런 준비 코드를 없앨 수 있다.

 

3) 위임 프로퍼티를 통해 프로퍼티 변경 통지를 받는 경우

 

먼저 위임 프로퍼티를 사용하기 위해 ObservableProperty에 있는 두 메소드의 시그니처를 코틀린 관례에 맞게 수정한다. 

 

class ObservableProperty(
    var propValue:Int, val changeSupport: PropertyChangeSupport
){
    operator fun getValue(p:Person, prop:KProperty<*>): Int = propValue
    operator fun setValue(p:Person, prop:KProperty<*>, newValue:Int){
        val oldValue = propValue
        propValue = newValue
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }
}

 

- 코틀린 관례. getValue, setValue 함수 모두 operator 변경자가 붙음.

- getValue, setValue는 프로퍼티가 포함된 객체(Person3), 프로퍼티를 표현하는 객체(KProperty 타입의 prop)를 파라미터로 받는다. KProperty.name으로 접근 가능하므로 name 프로퍼티는 없앤다.

 

class Person(val name:String, age:Int, salary: Int): PropertyChangeAware(){
    var age: Int by ObservableProperty(age, changeSupport)
    var salary: Int by ObservableProperty(salary, changeSupport)
}

 

- by 오른쪽에 오는 객체를 위임 객체라고 한다. 코틀린은 위임 객체를 감춰진 프로퍼티에 저장하고, 주 객체의 프로퍼티를 읽거나 쓸 때마다 위임 객체의 getValue와 setValue를 호출해준다.

 

4) 코틀린 표준 라이브러리를 이용해 프로퍼티 변경 통지 구현하기

 

class Person(val name:String, age:Int, salary: Int): PropertyChangeAware(){
    private val observer = {
        prop: KProperty<*>, oldValue:Int, newValue:Int -> changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }
    var age: Int by Delegates.observable(age, observer)
    var salary: Int by Delegates.observable(salary, observer)
}

 

이 표준 라이브러리의 클래스는 PropertyChangeSupport와는 연결되어 있지 않기 때문에 프로퍼티 값의 변경을 통지할 때 관련 람다를 클래스에 넘기고 있다.