관리 메뉴

나만을 위한 블로그

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

책/Effective Kotlin

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

참깨빵위에참깨빵_ 2022. 9. 11. 01:34
728x90
반응형

코틀린은 코드 재사용과 관련해서 프로퍼티 위임이라는 기능을 제공한다. 프로퍼티 위임을 쓰면 일반적인 프로퍼티의 행위를 추출해 재사용할 수 있다. 대표적인 예로 지연 프로퍼티가 있다. lazy 프로퍼티는 이후에 처음 사용하는 요청이 들어올 때 초기화되는 프로퍼티를 의미한다.

일반적으로 대부분의 언어(자바스크립트 등)에서는 필요할 때마다 복잡하게 구현해야 하지만 코틀린에선 프로퍼티 위임을 활용해 간단하게 구현할 수 있다. 코틀린의 stdlib는 lazy 프로퍼티 패턴을 쉽게 구현할 수 있게 lazy 함수를 제공한다.

 

val value by lazy { createValue() }

 

프로퍼티 위임을 쓰면 이외에도 변화가 있을 때 이를 감지하는 observable 패턴을 쉽게 만들 수 있다. 목록을 출력하는 리스트 어댑터가 있다면 변경 내용을 출력해야 할 경우 stdlib의 observable 델리게이트를 기반으로 간단하게 구현할 수 있다.

 

import android.util.Log
import kotlin.properties.Delegates

val items: List<Item> by Delegates.observable(listOf()) { _, _, _ ->
    notifyDataSetChanged()
}

val key: String? by Delegates.observable(null) { _, old, new ->
    Log.e("TAG", "key changed from $old to $new")
}


일반적으로 프로퍼티 위임 매커니즘을 활용하면 다양한 패턴들을 만들 수 있다. 뷰, 리소스 바인딩, 의존성 주입, 데이터 바인딩 등이 있다. 일반적으로 이런 패턴들을 사용할 때 자바 등에선 어노테이션을 많이 활용해야 한다. 하지만 코틀린은 프로퍼티 위임을 써서 간단하고 type-safe하게 구현할 수 있다.

어떻게 이런 코드가 가능하고 프로퍼티 위임을 어떻게 활용할 수 있는지 보기 위해 간단한 프로퍼티 델리게이트를 만든다.

예를 들어 일부 프로퍼티가 사용될 때 간단한 로그를 출력한다고 한다. 아래는 게터, 세터에서 로그를 출력하는 기본적인 구현 방법이다.

 

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
    }

 

두 프로퍼티는 타입이 다르지만 내부적으로 거의 같은 처리를 한다. 또한 자주 반복될 거 같은 패턴처럼 보여서 프로퍼티 위임을 활용해 추출하기 좋은 부분이다. 프로퍼티 위임은 다른 객체의 메서드를 활용해서 프로퍼티의 접근자(게터, 세터)를 만드는 방식이다. 이 때 다른 객체의 메서드명이 중요하다. 게터는 getValue, 세터는 setValue 함수를 써서 만들어야 한다. 객체를 만든 뒤에는 by 키워드를 써서 getValue, setValue를 정의한 클래스와 연결하면 된다.

 

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
    }
}

 

프로퍼티 위임이 어떻게 동작하는지 이해하려면 by가 어떻게 컴파일되는지 보는 게 좋다. getValue, setValue는 단순하게 값만 처리하게 바뀌는 게 아니라 컨텍스트(this)와 프로퍼티 레퍼런스의 경계도 함께 쓰는 형태로 바뀐다. 프로퍼티에 대한 레퍼런스는 이름, 어노테이션 관련 정보 등을 얻을 때 사용된다. 컨텍스트는 함수가 어떤 위치에서 사용되는지와 관련된 정보를 제공한다.

이런 정보로 인해 getValue, setValue가 여러 개 있어도 문제없다. 컨텍스트를 활용하므로 상황에 따라 적절한 메서드가 선택된다.

객체를 프로퍼티 위임하려면 val의 경우 getValue 연산, var의 경우 getValue, setValue 연산이 필요하다. 이런 연산은 확장 함수로도 만들 수 있다. 아래는 Map<String, *>를 쓰는 예시다.

 

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

fun main() {
    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

 

굉장히 범용적으로 쓰이는 패턴들에 대한 프로퍼티 델리게이터이므로 알아두면 좋다. 또한 프로퍼티를 직접 만들어 쓸 수 있다는 것도 기억하라.

반응형
Comments