위임 프로퍼티
위임 프로퍼티
어떤 흔한 종류의 프로퍼티는 필요할 때마다 매번 직접 구현할 수도 있지만, 한 번 구현해서 라이브러리에 넣고 나중에 재사용하는 편이 훨씬 유용해요. 예를 들면 이런 것들이죠.
- Lazy 프로퍼티: 값이 첫 접근 때만 계산돼요.
- Observable 프로퍼티: 리스너에게 이 프로퍼티의 변경이 통지돼요.
- 각 프로퍼티마다 별도의 필드를 두는 대신 맵에 프로퍼티를 저장하기.
이런(그리고 다른) 경우를 다루기 위해 Kotlin은 *위임 프로퍼티(delegated properties)*를 지원해요.
출처: Kotlin 공식 문서
본문
class Example {
var p: String by Delegate()
}
문법은 val/var <프로퍼티 이름>: <타입> by <표현식>이에요. by 뒤의 표현식이 *델리게이트(delegate)*예요. 프로퍼티에 대응하는 get()(그리고 set())이 델리게이트의 getValue()와 setValue() 메서드에 위임되기 때문이죠. 프로퍼티 델리게이트가 인터페이스를 구현해야 하는 건 아니지만, getValue() 함수(그리고 var라면 setValue()도)를 제공해야 해요.
예를 들어:
import kotlin.reflect.KProperty
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, thank you for delegating '${property.name}' to me!"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$value has been assigned to '${property.name}' in $thisRef.")
}
}
Delegate의 인스턴스에 위임하는 p에서 값을 읽으면 Delegate의 getValue() 함수가 호출돼요. 그 첫 매개변수는 p를 읽는 객체이고, 두 번째 매개변수는 p 자체에 대한 설명(예: 이름을 얻을 수 있음)을 담아요.
val e = Example()
println(e.p)
이 코드는 이렇게 출력해요.
Example@33a17727, thank you for delegating 'p' to me!
비슷하게 p에 값을 할당하면 setValue() 함수가 호출돼요. 처음 두 매개변수는 같고, 세 번째는 할당되는 값을 담아요.
e.p = "NEW"
이 코드는 이렇게 출력해요.
NEW has been assigned to 'p' in Example@33a17727.
위임 객체에 대한 요구사항의 명세는 아래에서 찾을 수 있어요.
위임 프로퍼티는 함수나 코드 블록 안에서도 선언할 수 있어요. 클래스의 멤버일 필요는 없죠. 예시는 아래에서 볼 수 있어요.
표준 델리게이트
Kotlin 표준 라이브러리는 몇 가지 유용한 델리게이트 종류에 대한 팩토리 메서드를 제공해요.
Lazy 프로퍼티
lazy()는 람다를 받아 Lazy 인스턴스를 돌려주는 함수예요. 이 Lazy 인스턴스가 lazy 프로퍼티를 구현하는 델리게이트 역할을 해요. get()을 처음 호출하면 lazy()에 전달된 람다가 실행되고 그 결과가 기억돼요. 이후의 get() 호출은 기억된 결과를 그대로 돌려줄 뿐이죠.
val lazyValue: String by lazy {
println("computed!")
"Hello"
}
fun main() {
println(lazyValue)
println(lazyValue)
}
기본적으로 lazy 프로퍼티의 평가는 *동기화(synchronized)*되어 있어요. 값은 한 스레드에서만 계산되지만, 모든 스레드가 같은 값을 보게 돼요. 여러 스레드가 동시에 실행하게 하려고 초기화 델리게이트의 동기화가 필요 없다면, lazy()에 매개변수로 LazyThreadSafetyMode.PUBLICATION을 전달하면 돼요.
초기화가 항상 프로퍼티를 사용하는 것과 같은 스레드에서 일어난다고 확신한다면 LazyThreadSafetyMode.NONE을 사용할 수 있어요. 스레드 안전성 보장과 관련 오버헤드가 전혀 없어요.
Observable 프로퍼티
Delegates.observable()은 두 인자를 받아요. 초기 값과 수정에 대한 핸들러죠.
핸들러는 프로퍼티에 할당할 때마다(할당이 이루어진 후에) 호출돼요. 세 매개변수를 받죠. 할당되는 프로퍼티, 이전 값, 새 값이에요.
import kotlin.properties.Delegates
class User {
var name: String by Delegates.observable("<no name>") {
prop, old, new ->
println("$old -> $new")
}
}
fun main() {
val user = User()
user.name = "first"
user.name = "second"
}
할당을 가로채서 *거부(veto)*하고 싶다면 observable() 대신 vetoable()을 쓰세요. vetoable에 전달한 핸들러는 새 프로퍼티 값의 할당 전에 호출돼요.
다른 프로퍼티에 위임하기
프로퍼티는 자신의 게터와 세터를 다른 프로퍼티에 위임할 수 있어요. 이런 위임은 최상위 및 클래스 프로퍼티(멤버와 확장) 모두에서 가능해요. 델리게이트 프로퍼티는 다음 중 하나일 수 있어요.
- 최상위 프로퍼티
- 같은 클래스의 멤버 또는 확장 프로퍼티
- 다른 클래스의 멤버 또는 확장 프로퍼티
프로퍼티를 다른 프로퍼티에 위임하려면 델리게이트 이름에 :: 한정자를 쓰세요. 예를 들어 this::delegate 또는 MyClass::delegate처럼요.
var topLevelInt: Int = 0
class ClassWithDelegate(val anotherClassInt: Int)
class MyClass(var memberInt: Int, val anotherClassInstance: ClassWithDelegate) {
var delegatedToMember: Int by this::memberInt
var delegatedToTopLevel: Int by ::topLevelInt
val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt
}
var MyClass.extDelegated: Int by ::topLevelInt
이 기능은 예를 들어 하위 호환 가능한 방식으로 프로퍼티를 이름 바꾸고 싶을 때 유용해요. 새 프로퍼티를 도입하고, 이전 프로퍼티에 @Deprecated 애너테이션을 붙이고, 그 구현을 위임하는 거예요.
class MyClass {
var newName: Int = 0
@Deprecated("Use 'newName' instead", ReplaceWith("newName"))
var oldName: Int by this::newName
}
fun main() {
val myClass = MyClass()
// Notification: 'oldName: Int' is deprecated.
// Use 'newName' instead
myClass.oldName = 42
println(myClass.newName) // 42
}
프로퍼티를 맵에 저장하기
흔한 사용 사례 중 하나는 프로퍼티의 값을 맵에 저장하는 거예요. JSON을 파싱하거나 다른 동적 작업을 할 때 애플리케이션에서 자주 나타나죠. 이런 경우 맵 인스턴스 자체를 위임 프로퍼티의 델리게이트로 쓸 수 있어요.
class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
이 예시에서 생성자는 맵을 받아요.
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))
위임 프로퍼티는 프로퍼티의 이름과 연관된 문자열 키를 통해 이 맵에서 값을 가져와요.
class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
fun main() {
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))
//sampleStart
println(user.name) // Prints "John Doe"
println(user.age) // Prints 25
//sampleEnd
}
읽기 전용 Map 대신 MutableMap을 쓰면 var 프로퍼티에서도 작동해요.
class MutableUser(val map: MutableMap<String, Any?>) {
var name: String by map
var age: Int by map
}
지역 위임 프로퍼티
지역 변수를 위임 프로퍼티로 선언할 수 있어요. 예를 들어 지역 변수를 lazy하게 만들 수 있죠.
fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)
if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}
memoizedFoo 변수는 첫 접근 때만 계산돼요. someCondition이 실패하면 그 변수는 전혀 계산되지 않아요.
프로퍼티 델리게이트 요구사항
읽기 전용 프로퍼티(val)의 경우 델리게이트는 다음 매개변수를 가진 연산자 함수 getValue()를 제공해야 해요.
thisRef는 *프로퍼티 소유자(property owner)*와 같은 타입이거나 그 슈퍼타입이어야 해요 (확장 프로퍼티의 경우 확장되는 타입이어야 해요).property는KProperty타입 또는 그 슈퍼타입이어야 해요.
getValue()는 프로퍼티와 같은 타입(또는 그 서브타입)을 돌려줘야 해요.
class Resource
class Owner {
val valResource: Resource by ResourceDelegate()
}
class ResourceDelegate {
operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
return Resource()
}
}
가변 프로퍼티(var)의 경우 델리게이트는 추가로 다음 매개변수를 가진 연산자 함수 setValue()를 제공해야 해요.
thisRef는 프로퍼티 소유자와 같은 타입이거나 그 슈퍼타입이어야 해요 (확장 프로퍼티의 경우 확장되는 타입이어야 해요).property는KProperty타입 또는 그 슈퍼타입이어야 해요.value는 프로퍼티와 같은 타입(또는 그 슈퍼타입)이어야 해요.
class Resource
class Owner {
var varResource: Resource by ResourceDelegate()
}
class ResourceDelegate(private var resource: Resource = Resource()) {
operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
return resource
}
operator fun setValue(thisRef: Owner, property: KProperty<*>, value: Any?) {
if (value is Resource) {
resource = value
}
}
}
getValue() 및/또는 setValue() 함수는 델리게이트 클래스의 멤버 함수로 제공하거나 확장 함수로 제공할 수 있어요. 후자는 원래 이 함수들을 제공하지 않는 객체에 프로퍼티를 위임해야 할 때 유용해요. 두 함수 모두 operator 키워드로 표시해야 해요.
Kotlin 표준 라이브러리의 인터페이스 ReadOnlyProperty와 ReadWriteProperty를 사용하면 새 클래스를 만들지 않고도 익명 객체로 델리게이트를 만들 수 있어요. 이 인터페이스들은 필요한 메서드를 제공하죠. getValue()는 ReadOnlyProperty에 선언되고, ReadWriteProperty는 그것을 확장하면서 setValue()를 추가해요. 즉 ReadOnlyProperty가 기대되는 곳이면 어디든 ReadWriteProperty를 전달할 수 있어요.
fun resourceDelegate(resource: Resource = Resource()): ReadWriteProperty<Any?, Resource> =
object : ReadWriteProperty<Any?, Resource> {
var curValue = resource
override fun getValue(thisRef: Any?, property: KProperty<*>): Resource = curValue
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Resource) {
curValue = value
}
}
val readOnlyResource: Resource by resourceDelegate() // ReadWriteProperty as val
var readWriteResource: Resource by resourceDelegate()
위임 프로퍼티의 변환 규칙
내부적으로 Kotlin 컴파일러는 일부 종류의 위임 프로퍼티에 대해 보조 프로퍼티를 생성한 다음 그것에 위임해요.
최적화 목적으로 컴파일러는 몇몇 경우 보조 프로퍼티를 생성하지 않습니다. 다른 프로퍼티에 위임하는 예시에서 최적화에 대해 배워 보세요.
예를 들어 프로퍼티 prop에 대해 컴파일러는 숨겨진 프로퍼티 prop$delegate를 생성하고, 접근자 코드가 이 추가 프로퍼티에 그냥 위임해요.
class C {
var prop: Type by MyDelegate()
}
// this code is generated by the compiler instead:
class C {
private val prop$delegate = MyDelegate()
var prop: Type
get() = prop$delegate.getValue(this, this::prop)
set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}
Kotlin 컴파일러는 인자에서 prop에 대한 필요한 모든 정보를 제공해요. 첫 번째 인자 this는 바깥 클래스 C의 인스턴스를 가리키고, this::prop은 prop 자체를 설명하는 KProperty 타입의 리플렉션 객체예요.
위임 프로퍼티의 최적화된 경우
$delegate 필드는 델리게이트가 다음일 때 생략돼요.
- 참조된 프로퍼티:
class C<Type> {
private var impl: Type = ...
var prop: Type by ::impl
}
- 이름 있는 객체:
object NamedObject {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String = ...
}
val s: String by NamedObject
- 같은 모듈에서 backing field와 기본 게터를 가진 final
val프로퍼티:
val impl: ReadOnlyProperty<Any?, String> = ...
class A {
val s: String by impl
}
- 상수 표현식, enum 항목,
this,null.this의 예시:
class A {
operator fun getValue(thisRef: Any?, property: KProperty<*>) ...
val s by this
}
다른 프로퍼티에 위임할 때의 변환 규칙
다른 프로퍼티에 위임할 때 Kotlin 컴파일러는 참조된 프로퍼티에 즉시 접근하는 코드를 생성해요. 즉 컴파일러는 prop$delegate 필드를 생성하지 않아요. 이 최적화 덕분에 메모리를 절약할 수 있어요.
예를 들어 다음 코드를 보세요.
class C<Type> {
private var impl: Type = ...
var prop: Type by ::impl
}
prop 변수의 프로퍼티 접근자는 위임 프로퍼티의 getValue와 setValue 연산자를 건너뛰고 impl 변수에 직접 접근해요. 그래서 KProperty 참조 객체도 필요 없어요.
위 코드에 대해 컴파일러는 다음 코드를 생성해요.
class C<Type> {
private var impl: Type = ...
var prop: Type
get() = impl
set(value) {
impl = value
}
fun getProp$delegate(): Type = impl // This method is needed only for reflection
}
델리게이트 제공하기
provideDelegate 연산자를 정의하면 프로퍼티 구현이 위임되는 객체를 만드는 로직을 확장할 수 있어요. by의 오른쪽에 있는 객체가 멤버나 확장 함수로 provideDelegate를 정의한다면, 그 함수가 프로퍼티 델리게이트 인스턴스를 만드는 데 호출돼요.
provideDelegate의 가능한 사용 사례 중 하나는 초기화 시 프로퍼티의 일관성을 확인하는 거예요.
예를 들어 바인딩 전에 프로퍼티 이름을 확인하려면 이렇게 쓸 수 있어요.
class ResourceDelegate<T> : ReadOnlyProperty<MyUI, T> {
override fun getValue(thisRef: MyUI, property: KProperty<*>): T { ... }
}
class ResourceLoader<T>(id: ResourceID<T>) {
operator fun provideDelegate(
thisRef: MyUI,
prop: KProperty<*>
): ReadOnlyProperty<MyUI, T> {
checkProperty(thisRef, prop.name)
// create delegate
return ResourceDelegate()
}
private fun checkProperty(thisRef: MyUI, name: String) { ... }
}
class MyUI {
fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { ... }
val image by bindResource(ResourceID.image_id)
val text by bindResource(ResourceID.text_id)
}
provideDelegate의 매개변수는 getValue의 매개변수와 같아요.
thisRef는 프로퍼티 소유자와 같은 타입이거나 그 슈퍼타입이어야 해요 (확장 프로퍼티의 경우 확장되는 타입이어야 해요).property는KProperty타입 또는 그 슈퍼타입이어야 해요.
provideDelegate 메서드는 MyUI 인스턴스를 만드는 동안 각 프로퍼티에 대해 호출되고, 필요한 검증을 즉시 수행해요.
프로퍼티와 델리게이트 사이의 바인딩을 가로채는 이 능력이 없다면, 같은 기능을 위해 프로퍼티 이름을 명시적으로 전달해야 해서 그리 편리하지 않아요.
// Checking the property name without "provideDelegate" functionality
class MyUI {
val image by bindResource(ResourceID.image_id, "image")
val text by bindResource(ResourceID.text_id, "text")
}
fun <T> MyUI.bindResource(
id: ResourceID<T>,
propertyName: String
): ReadOnlyProperty<MyUI, T> {
checkProperty(this, propertyName)
// create delegate
}
생성된 코드에서 provideDelegate 메서드는 보조 prop$delegate 프로퍼티를 초기화하기 위해 호출돼요. val prop: Type by MyDelegate() 선언에 대한 생성 코드와 위(만약 provideDelegate 메서드가 없을 때)를 비교해 보세요.
class C {
var prop: Type by MyDelegate()
}
// this code is generated by the compiler
// when the 'provideDelegate' function is available:
class C {
// calling "provideDelegate" to create the additional "delegate" property
private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
var prop: Type
get() = prop$delegate.getValue(this, this::prop)
set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}
provideDelegate 메서드는 보조 프로퍼티 생성에만 영향을 주고, 게터나 세터에 대해 생성되는 코드에는 영향을 주지 않는다는 점을 기억하세요.
표준 라이브러리의 PropertyDelegateProvider 인터페이스로 새 클래스를 만들지 않고도 델리게이트 제공자를 만들 수 있어요.
val provider = PropertyDelegateProvider { thisRef: Any?, property ->
ReadOnlyProperty<Any?, Int> {_, property -> 42 }
}
val delegate: Int by provider