본문 바로가기

Reading Record/이펙티브 코틀린

[이펙티브 코틀린] Item21. 일반적인 프로퍼티 패턴은 프로퍼티 위임으로 만들어라

코틀린은 프로퍼티 위임이라는 새로운 기능을 제공

  • 프로퍼티 위임을 사용하면 일반적인 프로퍼티의 행위를 추출해서 재사용할 수 있다.
  • 대표적으로 지연 프로퍼티인 lazy 프로퍼티가 존재
    • 처음 사용하는 요청이 들어올 때 초기화 되는 프로퍼티
    • val value by lazy { createValue() }
  • 변화가 있을 때 이를 감지하는 observable 패턴, 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 new $old to $new")
    }

프로퍼티 위임 메커니즘을 활용하면, 다양한 패턴들을 간단하고 type-safe하게 만들 수 있음

프로퍼티 위임을 통한 getter, setter 로깅

  • 프로퍼티 위임은 다른 객체의 메소드를 활용해서 프로퍼티의 접근자(게터와 세터)를 만드는 방식
  • 이때 다른 객체의 메소드 이름이 중요함
  • 게터는 getValue, 세터는 setValue 함수를 사용해서 만들어야 한다.
  • 객체를 만든 뒤에는 by 키워드를 사용해서, getVlaue와 setValue를 정의한 클래스와 연결해 주면 된다.
class LoggingProperty<T>(var value: T) {
    operator fun getValue(
            thisRef: Any?,
            prop: KProperty<*>
    ): T {
        println("${prop.name} returned vale $value")
        return value
    }

    operator fun setValue(
            thisRef: Any?,
            prop: KProperty<*>,
            newValue: T
    ) {
        val name = prop.name
        println("$name changed from $value to $newValue")
        value = newValue
    }

}

fun main() {
    var tokenaaa: String? by LoggingProperty(null)
    var attempts: Int by LoggingProperty(0)

    tokenaaa
    tokenaaa = "a"

    attempts
    attempts = 1
}

프로퍼티 위임이 어떻게 동작하는지 이해하려면, 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)
    }
  • 컨텍스트(this)와 프로퍼티 레퍼런스의 경계도 함께 사용하는 형태로 변경
  • 컨텍스트를 활용하기 때문에 getValue와 setValue가 여러 개 있어도 문제가 없다. 상황에 따라서 적절한 메소드가 선택됨
  • 위임 프로퍼티는 확장 함수로도 만들 수 있다.

코틀린 stdlib에서 알아 두면 좋은 프로퍼티 델리게이터

  • lazy -> 지연 초기화
  • Delegates.observable -> 프로퍼티의 데이터가 변할 때마다 callback을 받을 수 있다.
  • Delegates.vetoable -> observable과 거의 유사하지만 반환 값이 있다
  • Delgates.notNull -> 프로퍼티를 non-null 타입으로 변경