Developing Myself Everyday

프로퍼티 위임

프로퍼티 위임은 프로퍼티의 접근자(getter, setter) 구현을 다른 객체에게 위임하는 방식입니다. 이를 통해 반복적으로 사용되는 프로퍼티의 행위를 추출해서 재사용할 수 있습니다.


예를 들어 프로퍼티를 get할 때마다 반복적으로 특정 행위를 수행해야 한다면, getter에 해당하는 동작을 다른 객체에게 위임하여 대신 구현하는 방식으로 여러 프로퍼티에 공통적으로 적용할 수 있습니다.

 

대표적인 예로는 지연 프로퍼티가 있습니다.

val value by lazy { createValue() }

 

lazy 함수는 해당 프로퍼티를 처음 접근하려는 요청이 들어올 때 초기화를 수행하며, 이후에는 초기화된 값을 반환하는 동작을 제공합니다. 이 동작은 lazy 함수에 의해 생성된 객체가 프로퍼티의 getter에 해당하는 동작을 위임받아서 대신 구현함으로써 이루어 집니다.

 

 

간단한 프로퍼티 델리게이트

어떻게 이런 코드가 가능한지 알아보기 위해 간단한 프로퍼티 델리게이트를 만들어 보겠습니다. 예를 들어 일부 프로퍼티가 사용될 때, 간단한 로그를 출력하고 싶다고 가정해 보겠습니다.

 

아래의 프로퍼티는 타입이 다르지만, 내부적으로 동일한 처리를 합니다.

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: String? = null
	get() {
    	print("attempts returned value $field")
        return field
    }
    set(value) {
    	print("attempts changed from $field to $value")
        field = value
    }

 

 

아래의 객체는 `getValue()`로 프로퍼티의 getter를 `setValue()`로 프로퍼티의 setter를 만듭니다. 객체를 만든 뒤에는 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)
    }

 

위의 코드를 보면 알 수 있는 것은, getValue와 setValue는 단순히 값만 처리하게 바뀌는 것이 아니라, 컨텍스트(this)와 프로퍼티 참조도 함께 사용하는 형태로 바뀝니다.

 

위임 객체는 컨텍스트(this)를 통해 프로퍼티를 소유한 객체에 접근할 수 있고, 프로퍼티 참조는 프로퍼티에 대한 정보를 제공합니다.

 

이러한 특성 덕분에 아래와 같이 getValue 메서드가 여러개 있더라도 컨텍스트를 활용하므로, 상황에 따라 적절한 메서드가 선택됩니다.

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

 

 

Map과 확장함수를 이용한 프로퍼티 위임

Map는 아래와 같이 확장 함수가 정의되어 있어서 이를 활용할 수도 있습니다.

inline operator fun <V, V1 : V> Map<in String, V>.getValue(
    thisRef: Any?,
    property: KProperty<*>
): V1 = getOrImplicitDefault(property.name) as V1
val map: Map<String, Any> = mapOf(
    "name" to "Marcin",
    "kotlinProgrammer" to true
)
val name by map
println(name) // 출력: Marcin

 

 

자주 사용되는 프로퍼티 델리게이터

코틀린 stdilb에서 자주 사용되는 프로퍼티 델리게이터는 아래와 같습니다.

 

- lazy

- Delegates.observable

프로퍼티 값이 변경될 때마다 콜백을 호출합니다. 

import kotlin.properties.Delegates

var name: String by Delegates.observable("Unknown") { property, oldValue, newValue ->
    println("${property.name} 변경: $oldValue -> $newValue")
}

fun main() {
    name = "Alice"  // 출력: name 변경: Unknown -> Alice
    name = "Bob"    // 출력: name 변경: Alice -> Bob
}

 

- Delegates.vetoable

프로퍼티 값이 변경되기 전에 조건을 검사합니다.

import kotlin.properties.Delegates

var age: Int by Delegates.vetoable(18) { property, oldValue, newValue ->
    if (newValue >= 0) {
        true  // 변경 허용
    } else {
        println("나이는 음수가 될 수 없습니다!") 
        false // 변경 거부
    }
}

fun main() {
    age = 25  // 변경 허용
    println(age) // 출력: 25

    age = -5   // 변경 거부, 출력: 나이는 음수가 될 수 없습니다!
    println(age) // 출력: 25
}

 

- Delegates.notNull

프로퍼티에 null 값을 허용하지 않도록 강제합니다. 일반적으로 lateinit의 대체로 사용할 수 있습니다.

import kotlin.properties.Delegates

var username: String by Delegates.notNull()

fun main() {
    // println(username) // 초기화되지 않아 예외 발생: Exception: Property username should be initialized before get.

    username = "JohnDoe"
    println(username) // 출력: JohnDoe
}



profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!