[Effective Kotlin] 21 - 일반적인 프로퍼티 패턴은 프로퍼티 위임으로 만들어라
Kotlin은 코드 재사용과 관련해서 프로퍼티 위임이라는 새로운 기능을 제공합니다. 프로퍼티 위임을 사용하면 일반적인 프로퍼티의 행위를 추출해서 재사용할 수 있습니다.
대표적인 예로 지연 프로퍼티가 있습니다. lazy 프로퍼티는 이후에 처음 사용하는 요청이 들어올 때 초기화되는 프로퍼티를 의미합니다. 이러한 패턴은 굉장히 많이 사용됩니다. 일반적으로 대부분의 언어에서는 필요할 때마다 이를 복잡하게 구현해야 하지만, 코틀린에서는 프로퍼티 위임을 활용해 간단하게 구현할 수 있습니다. 코틀린의 stdlib는 lazy 프로퍼티 패턴을 쉽게 구현할 수 있게 lazy 함수를 제공합니다.
val value by lazy { createValue() }
프로퍼티 위임을 사용하면 이외에도 변화가 있을 때 이를 감지하는 observable패턴을 쉽게 만들 수 있습니다. 아래와 같이 stdlib의 observable 델리게이트를 기반으로 간단하게 구현할 수 있습니다.
var items: List<Item> by
Delegates.observable(listOf()) { _,._,_ ->
notifiyDataSetChagned()
}
lazy와 Observable 델리게이터는 언어적인 관점에서 보았을 때, 그렇게 특별한 것은 아닙니다. 일반적으로 프로퍼티 위임 메커니즘을 활용하면, 다양한 패턴들을 만들 수 있습니다. 일반적으로 이런 패턴들을 사용할 때 자바 등에서는 어노테이션을 많이 활용해야 합니다. 하지만 Kotlin은 프로퍼티 위임을 사용해서 간단하고 type-safe하게 구현할 수 있습니다.
어떻게 이런 코드가 가능하고, 프로퍼티 위임을 어떻게 활용할 수 있는지 살펴볼 수 있게, getter와 setter에서 간단한 로그를 출력하는 예를 들어보겠습니다.
var token: String? = null
get() {
print("token returned value $field")
return field
}
set() {
print(token changed from $field to $value)
field = value
}
var attempts: Int = 0
get() {
print("attempts returned value $field")
return field
}
set() {
print(attempts changed from $field to $value)
field = value
}
위의 예에서 두 프러퍼티 타입은 다르지만, 내부적으로 거의 같은 처리를 합니다. 또한 프로젝트에서 자주 반복될 것 같은 패턴처럼 보입니다. 따라서 프러퍼티 위임을 활용해서 추출하기 좋은 부분입니다.
프로퍼티 위임은 다른 개첵의 메서드를 활용해서 프로퍼티의 접근자(getter와 setter)를 만든느 방식입니다. 이때 다른 객체의 메서드 이름이 중요한데, getter는 getValue, setter는 setValue 함수를 사용해서 만들어야 합니다.
var token: String? by LoggingProperty(null)
var attempts: Int by LoggingProperty(null)
private class LoggingProperty<T>(var value: T) {
operator fun getValue(
thisRef: Any?,
prop: KProperty<*>
): T {
print("${prop.name} returned value $value")
return field
}
operator fun setValue(
thisRef: Any?,
prop: KProperty<*>,
newValue: T
) {
val name = prop.name
print($name changed from $value to $newValue)
value = newValue
}
}
위 코드는 위에서 살펴본 코드를 프로퍼티 위임을 활용해 변경한 예입니다.
프로퍼티 위임이 어떻게 동작하는지 이해하려면, by가 어떻게 컴파일 되는지 보는 것이 좋습니다.
위의 token 프로퍼티는 아래와 비슷한 형태로 컴파일됩니다.
(프로퍼티가 톱레벨에서 사용될 때는 this 대신 null로 바뀝니다.)
@JvmField
private val 'token$delegate' = LoggingProperty<String?>(null)
var token: String?
get() = 'token$delegate'.getValue(this, ::token)
set(value) {
'token$delegate'.setValue(this, ::token, value)
}
코드를 보면 알 수 있는 것처럼 getValue와 setValue는 단순하게 값만 처리하게 바뀌는 것이 아니라, 컨텍스트(this)와 프로퍼티 레퍼런스의 경계도 함께 사용하는 형태로 바뀝니다. 프로퍼티에 대한 레퍼런스는 이름, 어노테이션과 관련된 정보 등을 얻을 때 사용됩니다. 그리고 컨텍스트 함수가 어떤 위치에서 사용되는지와 관련된 정보를 제공해 줍니다.
이러한 정보로 인해서 getValue와 setValue메서드가 여러 개 있어도 문제 없습니다. getValue와 setValue 메서드가 여러개 있어도 컨텍스트를 활용하므로, 상황에 따라서 적절한 메서드가 선택됩니다. 이는 굉장히 다양하게 활용됩니다. 예를 들어 여러 종류의 뷰와 함께 사용할 수 있는 델리게이트가 필요한 경우를 생각해 보면, 컨텍스트의 종류에 따라서 적절한 메서드가 선택되게 만들 수 있습니다.
정리
프로퍼티 델리게이트는 프로퍼티와 관련된 다양한 조작을 할 수 있으며, 컨텍스트와 관련된 대부분의 정보를 갖습니다. 이러한 특성으로 인해서 다양한 프로퍼티의 동작을 추출해서 재사용할 수 있습니다.