반응형
Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

안드로이드 개발자 노트

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

Kotlin/이펙티브 코틀린

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

어리둥절범고래 2023. 11. 11. 14:21
반응형

코틀린은 코드 재사용과 관련해서 프로퍼티 위임이라는 새로운 기능을 제공합니다.

프로퍼티 위임을 사용하면 일반적인 프로퍼티의 행위를 추출해서 재사용할 수 있습니다.

대표적인 예로 지연 프로퍼티가 있으며, 코틀린의 stdlib는 lazy 프로퍼티 패턴을 쉽게 구현할 수 있게 lazy 함수를 제공합니다.

val value by lazy { createValue() }

 

프로퍼티 위임을 사용하면, 변화가 있을 때 이를 감지하는 observable 패턴을 쉽게 만들 수 있습니다.

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

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

 

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

// 안드로이드에서의 뷰와 리소스 바인딩
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)

// kotlin 에서의 종속성 주입
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)

 

다음은 프로퍼티 위임을 활용한 일부 프로퍼티가 사용될 때, 간단한 로그를 출력하는 예시입니다.

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 키워드를 사용해서 연결해 주면 됩니다.

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가 어떻게 컴파일되는지 보는 것이 좋습니다.

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

 

etValue와 setValue는 단순하게 값만 처리하도록 바뀌는 것이 아니라, 컨텍스트(this)와 프로퍼티 레퍼런스의 경계도 함께 사용하는 형태로 바뀝니다.
프로퍼티에 대한 레퍼런스는 이름, 어노테이션과 관련된 정보 등을 얻을 때 사용됩니다.

그리고 컨텍스트는 함수가 어떤 위치에서 사용되는지와 관련된 정보를 제공해줍니다.

이러한 정보로 인해서 getValue와 setValue메서드가 여러 개 있어도 문제가 없습니다.

getValue와 setValue 메서드가 여러개 있어도 컨텍스트를 활용하므로, 상황에 따라서 적절한 메서드가 선택됩니다.

이는 다양하게 활용되는데 여러 종류의 뷰와 함께 사용될 수 있는 델리게이트가 필요한 경우가 있습니다.

이는 다음과 같이 구현해서, 컨텍스트의 종류에 따라서 적절한 메서드가 선택되게 만들 수 있습니다.

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

 

객체를 프로퍼티 위임하려면 val의 경우 getValue연산, var의 경우 getValue와 setValue 연산이 필요합니다.
이러한 연산은 확장 함수로도 만들 수 있습니다.
다음 코드는 Map<String, *>를 사용하는 예입니다.

val map: Map<String, Any> = mapOf(
    "name" to "Marcin",
    "kotlinProgrammer" 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

 


정리

 

  • 프로퍼티 델리게이트는 프로퍼티와 관련된 다양한 조작을 할 수 있으며, 컨텍스트와 관련된 대부분의 정보를 갖는데 이러한 특징으로 인해서 다양한 프로퍼티의 동작을 추출해서 재사용할 수 있다.
  • 프로퍼티 위임은 프로퍼티 패턴을 추출하는 일반적인 방법이라 많이 사용되고 있다.
반응형