21장 - 일반적인 프로퍼티 패턴은 프로퍼티 위임으로 만들어라
프로퍼티 위임이란?
property delegate를 사용하는 것을 프로퍼티 위임이라 부릅니다
대표적으로 지연 프로퍼티가 있습니다.
lazy 프로퍼티는 이후에 처음 사용하는 요청이 들어올 때 초기화되는 프로퍼티를 의미합니다.
다른 언어에서는 대부분 이를 복잡하게 구현해야 하지만 코틀린에서는 lazy 함수를 활용하는 프로퍼티 위임으로 간단하게 구현할 수 있습니다.
또한 변화가 있을 때 이를 감지하는 observable 패턴을 쉽게 만들 수 있습니다.
예를 들어 목록을 출력하는 리스트 어댑터가 있다면 내부 데이터가 변경될 때마다 변경된 내용을 다시 출력해야 합니다.
이때 observable 델리게이트를 기반으로 간단하게 구현할 수 있습니다.
프로퍼티가 사용될 때 로그를 출력하는 예제
기본적인 구현 방법은 게터와 세터에서 로그를 출력하는 방법이 존재합니다.
이런 경우 여러 프로퍼티에서 거의 같은 처리를 하게 됩니다.
이를 프로퍼티 위임으로 추출해낼 수 있습니다.
fun main() {
var token: String? by LoggingProperty(null)
token = "hi"
println()
val getToken = token
//출력
//token changed from null to hi
//token returned value hi
}
private class LoggingProperty<T>(var value: T){
operator fun getValue(
thisRef: Any?,
prop: KProperty<*>,
): T{
print("${prop.name} returned value $value")
return value
}
operator fun setValue(
thisRef: Any?,
prop: KProperty<*>,
newValue: T,
) {
val name = prop.name
print("$name changed from $value to $newValue")
value = newValue
}
}
val의 경우에는 getValue만 구현하면 되지만 var경우에는 setValue까지 구현해주어야 합니다.
공식문서 : https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/get-value.html
KProperty란?
Kotlin에서는 리플랙션을 활용할 때 앞에 K를 붙입니다.
예를 들어 KClass처럼 활용합니다.
val x = 1
fun main() {
println(::x.get())
println(::x.name)
}
위의 코드에서는 :: 연산자를 사용해서 KProperty<Int>임을 나타냅니다.
다음과 같은 프로퍼티 델리케이터를 알아 두면 좋습니다.
- lazy
- Delegates.observable
- Delegates.vetoable
- Delegated.notnull
observable과 vetoable이란?
observable활용
class MyUser {
var name: String by Delegates.observable("초기값") {
prop, old, new -> println("$old 값이 $new 값으로 변경됩니다.")
}
}
fun main() {
val user = MyUser()
user.name = "홍길동"
user.name = "임꺽정"
}
//출력
//초기값 값이 홍길동 값으로 변경됩니다.
//홍길동 값이 임꺽정 값으로 변경됩니다.
vetoable활용
class MoreBiggerInt(initValue: Int) {
var value: Int by Delegates.vetoable(initValue) {
property, oldValue, newValue -> {
val result = newValue > oldValue
if(result) {
println("더 큰 값이므로 값을 변경합니다.")
} else {
println("작은 값이므로 변경을 취소합니다.")
}
result
}()
}
}
fun main() {
val storeBiggerInt = MoreBiggerInt(10)
storeBiggerInt.value = 20
println("${storeBiggerInt.value}")
storeBiggerInt.value = 5
println("${storeBiggerInt.value}")
}
vetoable 위임자를 사용한다면 특정한 조건에 따라 변경을 취소할 수 있습니다.
Delegates Object
public object Delegates {
public final fun <T : kotlin.Any> notNull(): kotlin.properties.ReadWriteProperty<kotlin.Any?, T> { /* compiled code */ }
public final inline fun <T> observable(initialValue: T, crossinline onChange: (kotlin.reflect.KProperty<*>, T, T) -> kotlin.Unit): kotlin.properties.ReadWriteProperty<kotlin.Any?, T> { /* compiled code */ }
public final inline fun <T> vetoable(initialValue: T, crossinline onChange: (kotlin.reflect.KProperty<*>, T, T) -> kotlin.Boolean): kotlin.properties.ReadWriteProperty<kotlin.Any?, T> { /* compiled code */ }
}
위에서 다루었던 observable과 vetoable이 존재합니다.
onChange에서 observable은 Unit을 반환하고 vetoable은 Boolean을 반환하기 때문에 위의 차이점이 발생합니다.
결론
굉장히 범용적으로 사용되는 프로퍼티 델리게이트들을 활용할 수 있으며 LoggingProperty 같이 델리게이터를 직접 만들어서 사용할 수도 있습니다.