코드 재사용을 지원하도록 코틀린이 소개한 한 가지 새로운 기능은 Property delegation이다.
이를 사용한 두 가지 예시를 살펴보자.
첫 번째로 코틀린에서는 stdlib 내부에 lazy라는 함수가 있다. 이것은 lazy property pattern을 구현한 property delegate를 return 한다.
val value by lazy { createValue() }
이렇게 사용하면 value라는 property가 사용되는 시점에 createValue가 호출된 뒤에 초기화 된다.
두 번째는 observer 패턴이다. 안에 있는 데이터가 바뀌면 그에 맞춰서 item들을 다시 그려야 하는 상황이 있거나 모든 변화에 대한 로그를 남겨야한다고 생각해보면 stdlib의 observable 패턴을 구현해서 해결할 수 있다.
var items: List<Item> by
Delegates.observable(listOf()) { _, _, _ ->
notifyDataSetChanged()
}
var key: String? by
Delegates.observable(null) { _, old, new ->
Log.e("key changed from $old to $new")
}
Java에서 View, Resource Binding, Dependency Injection 혹은 Data Binding을 하려면 annotation processing과 같은 작업이 필요하지만, kotlin에서는 property delegation을 통해 쉽고 type-safe하게 구현하도록 할 수 있다.
// View and resource binding example in Android
private val button: Button by bindView(R.id.button)
private val textSize: by bindDimension(R.dimen.font_size)
// Dependency Injection using Kotlin
private val presenter: MainPresenter by inject()
private val vm: MainViewModel by viewModel()
// Data binding
private val port by bindConfiguration("port")
private val token: String by preferences.bind(TOKEN_KEY)
이게 어떻게 동작하는 지 알아보기 위해서 쉬운 것 부터 살펴보자.
우리가 property들이 어떤 식으로 사용되는 지 추적하기 위해 로그를 찍는 custom getter, setter를 추가했다고 가정하자.
var token: String? = null
get() {
print("token returened value $field")
return field
}
set(value) {
print("token changed from $field to $value")
field = value
}
var attemps: Int = 0
get() {
print("attemps returened value $field")
return field
}
set(value) {
print("attemps changed from $field to $value")
field = value
}
두 property의 타입은 다르지만 행위는 거의 동일하다고 볼 수 있다.
이 비슷한 두 가지의 행위는 property delegation으로 추출되서 사용할 수 있다. Delegation은 property가 그 val의 getter나 var의 getter, setter와 같은 accessor에 의해 정의된다는 아이디어에서 비롯되었다.
이러한 메소드들은 다른 오브젝트의 메소드로 위임될 수 있다.
var token: String? by LoggingProperty(null)
var attempts: Int by LoggingProperty(0)
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
}
}
KProperty - Kotlin Programming Language
kotlinlang.org
Property delegation이 작동하는 원리를 완벽히 알기 위해 "by"가 어떻게 컴파일 되는지 살펴보자.
@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는 value에만 작동하는게 아니라 property, context(this)에 대한 제한적 참조도 가능하다.
여러 개의 getValue와 setValue 메소드가 있으면 context에 의해 적절한 메소드를 찾아준다.
class SwipeRefreshBinderDelegate(val id: Int) {
private var cache: SwipeRefreshLayout? = null
operator fun getValue(
activity: Activity,
prop: Kproperty<*>
): SwipeRefreshLayout {
return cache ?: activity
.findViewById<SwipeRefreshLayout>(id)
.also { cache = it }
}
operator fun getValue(
fragment: Fragment,
prop: Kproperty<*>
): SwipeRefreshLayout {
return cache ?: fragment.view
.findViewById<SwipeRefreshLayout>(id)
.also { cache = it }
}
}
Object를 property delegate로 사용하기 위해서는 필요한 것은 val는 getValue, var은 getValue, setValue 연산자이다. 이런 연산자는 멤버 함수가 될 수 있지만 kotlin stdlib에 있는 extension 일 수도 있다.
val map: Map<String, Any> = mapOf(
"name" to "Marcin",
"kotlinProgrammer" to true
)
val name by map
print(name) // Marcin
inline operator fun <V, V1: V> Map<in String, V>
.getValue(thisRef: Any?, property: KProperty<*>): V1=
getOrImplicitDefault(property.name) as V1
Kotlin standard library에는 반드시 알아야 할 몇 가지 property delegate가 있다.
- lazy
- Delegates.observable
- Delegates.vetoable
- Delegates.notNull
'개발 > Kotlin' 카테고리의 다른 글
Item 49. Consider using inline classes (0) | 2021.11.04 |
---|---|
Item 42: Respect the contract of equals (0) | 2021.10.21 |
Item 35: Consider defining a DSL for complex object creation (0) | 2021.09.23 |
Item 28: Specify API stability (0) | 2021.09.16 |