아이템 21. 일반적인 프로퍼티 패턴은 프로퍼티 위임으로 만들어라

아이템 21. 일반적인 프로퍼티 패턴은 프로퍼티 위임으로 만들어라

일반적인 프로퍼티 패턴은 프로퍼티 위임으로 만들어라 #

프로퍼티 위임을 사용하면 일반적인 프로퍼티 행위를 추출해서 재사용할 수 있다. 또, 프로퍼티 위임 메커니즘을 통해 다양한 패턴들을 만들 수 있다.

프로퍼티 위임은 프로퍼티 패턴을 추출하는 일반적인 방법이라 많이 사용되고 있다.

lazy, observable, 뷰, 리소스 바인딩, 의존성 주입, 데이터 바인딩 등의 사용 예시가 있겠다.

// 예시 : lazy

val value by lazy { createValue() }
// 예시 : observable

var items: List<Item> by
    Delegates.observable(listof()) { _, _, _ ->
        notifyDataSetChanged()
    }
// 예시 : 뷰, 리소스 바인딩 (안드로이드)

private val button: Button by bindView(R.id.button)
private val textSize by bindDimension(R.dimen.font_size)
private val doctor: Doctor by argExtra(DOCTOR_ARG)
// 예시 : 의존성 주입 (Koin)

private val presenter: MainPresenter by inject()
private val repository: NetworkRepository by inject()
private val vm: MainViewModel by viewModel()
// 예시 : 데이터 바인딩

private val port by bindConfiguration("port")
private val token: String by preferences.bind(TOKEN_KEY)

커스텀 프로퍼티 위임 만들어보기 #

프로퍼티 위임은 다른 객체의 메서드를 활용해서 프로퍼티의 접근자(getter/setter)를 만드는 방식이다.

게터, 세터 사용 시 로그를 출력하는 예시가 있다고 가정해보자.

var token: String? = null
    get() {
        print("token returned value $field")
        return field
    }
    set(value) {
        print("token changed from $field to $value")
        field = value
    }

var attempts: Int = 0
    get() {
        print("attempts returned value $field")
        return field
    }
    set(value) {
        print("attempts changed from $field to $value")
        field = value
    }

위 코드를 프로퍼티 위임을 통해 개선해보자. (확장 함수로 만들 수도 있다.)

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
    }
}
private class LoggingProperty<T>(var value: T) {
    operator fun getValue(
        thisRef: 
    )
}

getValue, setValue 메서드가 여러 개 있어도 무방하다. (오버로딩 형태로 생각해면 될까 싶다.)


위의 코드 중 by가 어떻게 컴파일되는지 살펴보자.

// var token: String? by LoggingProperty(null)
// var attempts: Int by LoggingProperty(0)

@JvmField
private val 'token$delegate' =
    LoggingProperty<String?>(null)

var token: String?
    get() = 'token$delegate'.getValue(thisRef = this, prop = ::token)
    set(value) {
        'token$delegate'.setValue(
            thisRef = this, 
            prop = ::token, 
            newValue = value
        )
    }
  • val 의 경우, getValue 가 필요하다.
  • var 의 경우, getValue, setValue 가 필요하다.

그 외 예시 #

val map: Map<String, Any> = mapOf(
    "name" to "Marcin",
    "programmer" to true
)

val name by map
print(name) // Marcin

위 코드는 코틀린 stdlib 에 확장 함수가 정의되어 있기에 사용할 수 있는 코드다.

inline operator fun <V, V1 : V> Map<in String, V>.getValue(thisRef: Any?, property: KProperty<*>): V1 = 
    getOrImplicitDefault(property.name) as V1

코틀린 stdlib의 다음과 같은 프로퍼티 위임을 알아 두면 좋다.

  • lazy
  • Delegates.observable
  • Delegates.vetoable
  • Delegates.notNull

굉장히 범용적으로 사용되는 델리게이터들이다.

참고 : https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.properties/-delegates/


참고 : notNull #

var max: Int by Delegates.notNull()

// println(max) // will fail with IllegalStateException

max = 10
println(max) // 10

참고 : observable #

var observed = false
var max: Int by Delegates.observable(0) { property, oldValue, newValue ->
    observed = true
}

println(max) // 0
println("observed is ${observed}") // false

max = 10
println(max) // 10
println("observed is ${observed}") // true

참고 : vetoable #

값 변경 시 특정 조건에 따라 변경을 취소할 수 있다.

inline fun <T> vetoable(
    initialValue: T,
    crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Boolean
): ReadWriteProperty<Any?, T>
(source)
var max: Int by Delegates.vetoable(0) { property, oldValue, newValue ->
    newValue > oldValue
}

println(max) // 0

max = 10
println(max) // 10

max = 5
println(max) // 10