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와는 연결되어 있지 않기 때문에 프로퍼티 값의 변경을 통지할 때 관련 람다를 클래스에 넘기고 있다.
'Dev Book > Kotlin IN ACTION' 카테고리의 다른 글
CH7(7.4). 구조 분해 선언과 component 선언 (0) | 2022.03.03 |
---|---|
CH7(7.3). 컬렉션과 범위에 대한 관례 (0) | 2022.02.18 |
CH7(7.2). 비교 연산자 오버로딩 (0) | 2022.02.15 |
CH7(7.1). 산술 연산자 오버로딩 (0) | 2022.02.13 |
CH6(6.3). 컬렉션과 배열 (0) | 2022.02.06 |