Search

Delegated properties

위임된 프로퍼티는 자주 사용되는 특정 종류의 프로퍼티를 반복해서 구현할 필요 없이 한 번만 정의하고 재사용할 수 있게 도와준다.

위임된 프로퍼티란?

val/var <프로퍼티 이름>: <타입> by <위임 객체>
Kotlin
복사
프로퍼티를 선언할 때 by 키워드를 사용하여 특정 객체에 그 역할을 맡긴다.
위임된 프로퍼티는 프로퍼티의 getter와 setter 로직을 다른 객체에 위임하는 방식이다.
위임 객체는 getValue() 및 setValue() 메서드를 제공해야한다.
class Example { var p: String by Delegate() }
Kotlin
복사
p라는 프로퍼티는 Delegate() 객체에 의해 관리된다.
즉, p를 읽거나 쓸 때 Delegate 객체의 메서드가 호출된다.
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.") } } class Example { var p: String by Delegate() } fun main() { val e = Example() println(e.p) // 읽기: getValue() 호출 e.p = "NEW" // 쓰기: setValue() 호출 } // 실행 결과 Example@33a17727, thank you for delegating 'p' to me! NEW has been assigned to 'p' in Example@33a17727.
Kotlin
복사
thisRef는 프로퍼티를 소유한 객체이다. (여기서는 Example 객체)
property는 프로퍼티의 메타정보를 포함한 객체이다. (여기서는 p 프로퍼티)
value는 할당된 값이다. (여기서는 “NEW”)

위임된 프로퍼티의 종류

지연 초기화 (Lazy properties)

프로퍼티의 값을 처음 접근할 때만 계산하고 그 이후로는 저장된 값을 사용하는 방식이다.
return이 생략된 형태로 반환된 값이 그대로 저장되어 두번째부턴 블록이 실행되지 않는다.
val lazyValue: String by lazy { println("Computed!") "Hello" } fun main() { println(lazyValue) // "Computed!" 출력, 그 후 "Hello" 출력 println(lazyValue) // "Hello"만 출력 (다시 계산되지 않음) }
Kotlin
복사

변경 감지 프로퍼티 (Observable properties)

프로퍼티가 변경될 때마다 특정 동작을 수행할 수 있다.
import kotlin.properties.Delegates class User { var name: String by Delegates.observable("Unknown") { prop, old, new -> println("Property '${prop.name}' changed from '$old' to '$new'") } } fun main() { val user = User() user.name = "Alice" // 출력: Property 'name' changed from 'Unknown' to 'Alice' user.name = "Bob" // 출력: Property 'name' changed from 'Alice' to 'Bob' }
Kotlin
복사

검증 프로퍼티 (Vetoable properties)

프로퍼티의 값이 변경되기 전에 특정 검사를 수행하고 검사가 통과되지 않으면 변경을 막는다.
import kotlin.properties.Delegates class User { var age: Int by Delegates.vetoable(0) { _, old, new -> if (new >= 0) { true // 값 변경 허용 } else { false // 값 변경 거부 } } } fun main() { val user = User() println(user.age) // 0 (초기값) user.age = 25 // 값 변경 허용 println(user.age) // 25 user.age = -1 // 값 변경 거부 (음수는 허용되지 않음) println(user.age) // 25 (변경되지 않음) }
Kotlin
복사

맵을 이용한 위임 프로퍼티

프로퍼티를 별도의 필드에 저장하는 대신 맵에 저장하여 동적으로 관리할 수 있다.
// Map 을 사용한 예시 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 )) println(user.name) // "John Doe" 출력 println(user.age) // 25 출력 } // MutuableMap 을 사용한 예시 class MutableUser(val map: MutableMap<String, Any?>) { var name: String by map var age: Int by map } fun main() { val mutableMap = mutableMapOf( "name" to "Jane Doe", "age" to 30 ) val user = MutableUser(mutableMap) println(user.name) // "Jane Doe" 출력 println(user.age) // 30 출력 user.name = "John Smith" // 맵의 값 변경 user.age = 40 // 맵의 값 변경 println(user.name) // "John Smith" 출력 println(user.age) // 40 출력 }
Kotlin
복사

다른 프로퍼티에 위임

프로퍼티의 getter와 setter를 다른 프로퍼티에 위임할 수 있다.
상위 수준 프로퍼티, 클래스의 멤버 프로퍼티, 확장 프로퍼티 등 다양한 프로퍼티에 사용할 수 있다.
프로퍼티를 다른 프로퍼티에 위임하려면 :: 연산자를 사용하여 위임할 프로퍼티를 참조한다.
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 fun main() { val anotherClass = ClassWithDelegate(10) val myClass = MyClass(5, anotherClass) println(myClass.delegatedToMember) // 5 println(myClass.delegatedToTopLevel) // 0 (초기값) println(myClass.delegatedToAnotherClass) // 10 myClass.extDelegated = 20 println(myClass.extDelegated) // 20 println(topLevelInt) // 20 }
Kotlin
복사
상위 수준 프로퍼티 위임
var topLevelInt: Int = 0 는 상위 수준 프로퍼티이다.
MyClass의 var delegatedToTopLevel: Int by ::topLevelInt 는 이 상위 수준 프로퍼티로 위임된다.
delegatedToTopLevel의 getter와 setter는 topLevelInt에 의해 처리된다.
클래스 멤버 프로퍼티 위임
MyClass의 memberInt는 클래스 멤버 프로퍼티이다.
MyClass의 var delegatedToMember: Int by this::memberInt 는 이 클래스 멤버 프로퍼티로 위임된다.
delegatedToMember의 getter와 setter는 memberInt에 의해 처리된다.
다른 클래스의 프로퍼티 위임
ClassWithDelegate의 anotherClassInt는 다른 클래스 멤버 프로퍼티이다.
MyClass의 val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt 는 이 다른 클래스 멤버 프로퍼티로 위임된다.
delegatedToAnotherClass의 getter와 setter는 anotherClassInt에 의해 처리된다.
확장 프로퍼티 위임
var MyClass.extDelegated: Int by ::topLevelInt 는 MyClass의 확장 프로퍼티로 topLevelInt에 위임된다.
class MyClass { var newName: Int = 0 @Deprecated("Use 'newName' instead", ReplaceWith("newName")) var oldName: Int by this::newName } fun main() { val myClass = MyClass() // 'oldName'은 deprecated된 프로퍼티입니다. // 'newName'을 대신 사용하세요. myClass.oldName = 42 println(myClass.newName) // 42 }
Kotlin
복사
다른 프로퍼티로의 위임을 이용하여 @Deprecated 된 프로퍼티의 호환성을 유지할 수 있다.

위임된 지역 프로퍼티

지역 변수도 위임된 프로퍼티로 선언할 수 있다.
fun example(computeFoo: () -> Foo) { val memoizedFoo by lazy(computeFoo) if (someCondition && memoizedFoo.isValid()) { memoizedFoo.doSomething() } }
Kotlin
복사
someCondition이 true인 경우에만 memoizedFoo가 초기화된다.

프로퍼티 위임 요구사항

프로퍼티 위임을 사용하려면 위임 객체는 특정 요구 사항을 충족해야 한다.
요구사항은 프로퍼티가 읽기 전용(val) 인지 읽기/쓰기 가능(var) 인지에 따라 다르다.

읽기 전용 프로퍼티(val)의 요구사항

getValue() 함수
thisRef
프로퍼티를 소유한 객체의 타입과 같거나 상위 타입이어야 한다.
확장 프로퍼티의 경우 확장되는 타입어어야 한다.
property
KProperty<*> 타입이거나 또는 그 상위 타입이어야 한다.
반환값
프로퍼티의 타입 또는 그 하위 타입이어야 한다.
class Resource class Owner { val valResource: Resource by ResourceDelegate() } class ResourceDelegate { operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource { return Resource() } }
Kotlin
복사

읽기/쓰기 가능 프로퍼티(var)의 요구사항

getValue() 함수
위와 동일
setValue() 함수
thisRef
프로퍼티를 소유한 객체의 타입과 같거나 상위 타입어어야 한다.
확장 프로퍼티의 경우 확장되는 타입어어야 한다.
property
KProperty<*> 타입이거나 또는 그 상위 타입어어야 한다.
반환값
프로퍼티의 타입 또는 그 하위 타입이어야 한다.
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 } } }
Kotlin
복사

위임 객체를 익명 객체로 만들기

위임 객체를 익명 객체로 만들어서 클래스를 별도로 생성하지 않고도 위임할 수 있다.
Kotlin 표준 라이브러리의 ReadOnlyProperty, ReadWriteProperty 인터페이스를 사용할 수 있다.
ReadOnlyProperty
읽기 전용 프로퍼티를 위한 getValue() 메서드가 정의되어 있다.
ReadWriteProperty
ReadOnlyProperty를 확장하며 읽기/쓰기 프로퍼티를 위한 setValue() 메서드를 추가한다.
import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty 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() // 읽기 전용 프로퍼티 var readWriteResource: Resource by resourceDelegate() // 읽기/쓰기 가능 프로퍼티
Kotlin
복사

위임된 프로퍼티 변환 규칙

Kotlin에서 위임된 프로퍼티를 사용할 때 컴파일러는 효율성을 높이기 위해 몇 가지 최적화를 수행한다.
이 최적화는 주로 추가적인 프로퍼티와 같은 보조 요소를 생성하거나 필요에 따라 이러한 요소를 생략하는 방식으로 이루어진다.

기본 변환 규칙

Kotlin 컴파일러는 위임된 프로퍼티를 다음과 같이 번역한다.
class C { var prop: Type by MyDelegate() }
Kotlin
복사
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$delegate
위임 객체를 저장하는 보조 프로퍼티이다.
getValue
prop에 접근할 때 호출되는 메서드이다.
setValue
prop의 값을 설정할 때 호출되는 메서드이다.

최적화된 변환 규칙

참조된 프로퍼티의 경우
impl 프로퍼티를 직접 참조하므로 prop$delegate 필드를 생성하지 않는다.
class C<Type> { private var impl: Type = ... var prop: Type by ::impl }
Kotlin
복사
명명된 객체의 경우
NamedObject가 직접 위임을 처리하므로 prop$delegate 필드가 필요 없다.
object NamedObject { operator fun getValue(thisRef: Any?, property: KProperty<*>): String = ... } val s: String by NamedObject
Kotlin
복사
같은 모듈 내에서 기본 getter가 있는 final val 프로퍼티의 경우
impl이 기본 getter를 제공하므로 prop$delegate 필드를 생성하지 않는다.
val impl: ReadOnlyProperty<Any?, String> = ... class A { val s: String by impl }
Kotlin
복사
상수 표현식, enum 항목, this, null 등의 경우
this가 위임의 대상이므로 prop$delegate 필드가 필요 없다.
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 }
Kotlin
복사
class C<Type> { private var impl: Type = ... var prop: Type get() = impl set(value) { impl = value } fun getProp$delegate(): Type = impl // 주로 Reflection용으로 사용 }
Kotlin
복사
get() 과 set()
prop의 접근자는 impl 변수에 직접 접근한다.
getValue와 setValue 메서드를 호출하지 않고 impl 프로퍼티에 직접 접근한다.
Reflection용 메서드
컴파일러는 getProp$delegate() 와 같은 메서드를 생성할 수 있지만 주로 리플렉션 용도로 사용된다.

위임 제공

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) // 실제 위임 객체 생성 return ResourceDelegate() } private fun checkProperty(thisRef: MyUI, name: String) { // 프로퍼티 이름 확인 로직 } } class MyUI { fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { // ResourceLoader 객체 생성 } val image by bindResource(ResourceID.image_id) val text by bindResource(ResourceID.text_id) }
Kotlin
복사
image, text 프로퍼티가 선언될 때 bindResource()가 호출되고 ResourceLoader 객체를 반환한다.
ResourceLoader 클래스는 provideDelegate 를 구현하고 있다.
provideDelegate의 매개변수는 thisRef(프로퍼티 소유 객체)와 prop(프로퍼티 설명 객체)이다.
provideDelegate는 bindResource가 호출될 때 프로퍼티의 이름을 검사한 후 ResourceDelegate를 생성하여 반환한다.
image, text 프로퍼티에 대한 접근이 발생하면 ResourceDelegate의 getValue 함수가 호출된다.

변환 규칙

provideDelegate 사용되지 않는 경우
class C { var prop: Type by MyDelegate() } // 컴파일러 생성 코드 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
복사
provideDelegate 사용되는 경우
class C { 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) }
Kotlin
복사
provideDelegate를 사용하면 컴파일러는 prop$delegate 필드를 생성할 때 provideDelegate를 호출하여 위임 객체를 설정한다.
prop$delegate 필드가 실제로는 provideDelegate 의 반환값을 가리키게 된다.

PropertyDelegateProvider 사용하기

Kotlin 표준 라이브러리에는 PropertyDelegateProvider 인터페이스가 있다.
PropertyDelegateProvider를 사용하면 새로운 클래스를 만들지 않고도 프로퍼티 위임 제공자를 생성할 수 있다.
PropertyDelegateProvider를 사용하면 프로퍼티의 위임을 간편하게 처리할 수 있다.
val provider = PropertyDelegateProvider { thisRef: Any?, property -> ReadOnlyProperty<Any?, Int> { _, property -> 42 } } val delegate: Int by provider
Kotlin
복사