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