프로퍼티 위임
프로퍼티 위임은 프로퍼티의 접근자(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
}
'스터디 > 이펙티브 코틀린' 카테고리의 다른 글
아이템 25 - 공통 모듈을 추출해서 여러 플랫폼에서 재사용하라 (1) | 2025.01.22 |
---|---|
아이템 18 - "코딩 컨벤션을 지켜라" (0) | 2025.01.06 |
아이템 10- "단위 테스트를 만들어라" (0) | 2024.12.23 |
아이템 1 - "가변성을 제한하라" (0) | 2024.12.09 |
[이펙티브 코틀린] 2장 - 가독성 (0) | 2024.01.28 |