본문 바로가기

Language/Kotlin

코틀린[Kotlin]에서 Delegated properties 사용하기

반응형

위임된 프로퍼티 (Delegated properties)

Kotlin은 특정 객체에 대한 프로퍼티의 set과 get 메서드의 호출을 위임할 수 있는 Delegated properties 메커니즘을 제공합니다.

이 경우 delegate 객체에는 getValue 메서드가 있어야 합니다. 변경 가능한 프로퍼티의 경우 setValue도 필요합니다.

import kotlin.reflect.KProperty

class Example {
    var p: String by Delegate()                                               // 1

    override fun toString() = "Example Class"
}

class Delegate() {
    operator fun getValue(thisRef: Any?, prop: KProperty<*>): String {        // 2     
        return "$thisRef, thank you for delegating '${prop.name}' to me!"
    }

    operator fun setValue(thisRef: Any?, prop: KProperty<*>, value: String) { // 2
        println("$value has been assigned to ${prop.name} in $thisRef")
    }
}

fun main() {
    val e = Example()
    println(e.p)
    e.p = "NEW"
}
  1. String 유형의 속성 p를 Delegate 클래스의 인스턴스에 위임합니다. delegate 객체는 by 키워드 뒤에 정의됩니다.
  2. 위임 메소드(Delegation Method). 이러한 메서드의 파라미터 또는 형식은 예제와 비슷합니다. 구현에는 필요한 단계가 포함될 수 있습니다. 불변 속성의 경우 getValue만 필요합니다.

 

일반적인 종류의 프로퍼티들이 있습니다. 필요할 때마다 수동으로 구현할 수 있지만 한 번 구현하고 라이브러리에 추가하면 도움이 됩니다. 예는 다음과 같습니다.

게으른 속성: 값은 처음 액세스할 때만 계산됩니다. 관찰 가능한 속성: 수신기는 이 속성의 변경 사항에 대해 알림을 받습니다. 각 속성에 대한 별도의 필드 대신 맵에 속성을 저장합니다.

 

표준 대리자 (Standard Delegates)

Kotlin 표준 라이브러리는 몇 가지 유용한 종류의 대리자를 위한 팩토리 메서드를 제공합니다.

 

Lazy properties

lazy()는 람다를 가지고 lazy 프로퍼티을 구현하기 위한 대리자 역할을 할 수 있는 Lazy<T>의 인스턴스를 반환하는 함수입니다.

get()에 대한 첫 번째 호출은 lazy()에 전달된 람다를 실행하고 결과를 기억하며, get()에 대한 후속 호출은 단순히 기억된 결과를 반환합니다.

val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}

fun main() {
    println(lazyValue)
    println(lazyValue)
}
// 호출 결과
// computed!
// Hello
// Hello

기본적으로 지연 프로퍼티의 값은 동기화됩니다. 값은 하나의 스레드에서만 계산되고 모든 스레드는 동일한 값을 보게 됩니다. 초기화 대리자의 동기화가 필요하지 않고, 여러 스레드가 동시에 실행할 수 있도록 하려면 LazyThreadSafetyMode.PUBLICATION을 lazy() 함수에 매개변수로 전달합니다.

 

초기화가 항상 프로퍼티를 사용하는 스레드와 동일한 스레드에서 발생한다고 확신하는 경우 LazyThreadSafetyMode.NONE을 사용할 수 있습니다. 스레드 안전 보장 및 관련 오버헤드가 발생하지 않습니다.

 

Observable properties

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"
}
// 출력 결과
// <no name> -> first
// first -> second

할당을 가로채 거부하려면 observable() 대신 vetoable()을 사용하세요. vetoable에 전달된 핸들러는 새 속성 값을 할당하기 전에 호출됩니다.

 

다른 프로퍼티에 위임하기

하나의 프로퍼티는 자신의 getter 및 setter를 다른 프로퍼티에 위임할 수 있습니다. 이러한 위임은 최상위 및 클래스 프로퍼티(멤버 및 확장) 모두에 사용할 수 있습니다. 대리자 프로퍼티는 다음과 같을 수 있습니다.

  • 최상위 프로퍼티
  • 같은 클래스의 멤버 또는 확장 프로퍼티
  • 다른 클래스의 멤버 또는 확장 프로퍼티

프로퍼티를 다른 프로퍼티에 위임하려면 대리자 이름에 적절한 :: 한정자를 사용해야 합니다.(예: 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
))

위임된 속성은 이 맵에서 값을 가져옵니다(문자열 키 - 속성 이름):

println(user.name) // Prints "John Doe"
println(user.age)  // Prints 25

읽기 전용 Map 대신 MutableMap을 사용하는 경우 var 속성에도 적용됩니다.

class MutableUser(val map: MutableMap<String, Any?>) {
    var name: String by map
    var age: Int     by map
}

 

로컬 위임 속성 (Local delegated properties)

지역 변수를 위임된 프로퍼티로 선언할 수 있습니다. 예를 들어, 지역 변수를 lazy 하게 만들 수 있습니다.

fun example(computeFoo: () -> Foo) {
    val memoizedFoo by lazy(computeFoo)

    if (someCondition && memoizedFoo.isValid()) {
        memoizedFoo.doSomething()
    }
}

memoizedFoo변수를 사용하려고 할 때만 작동됩니다. someCondition이 false이면 변수가 전혀 작동하지 않습니다.

 

프로퍼티 대리인 요구 사항

속성 위임에 대한 요구 사항은 다음과 같습니다.

읽기 전용 속성(val)의 경우 대리자는 다음 매개변수와 함께 연산자 함수 getValue()를 제공해야 합니다.

  • thisRef는 속성 소유자와 같거나 상위 유형이어야 합니다(확장 속성의 경우 확장되는 유형이어야 함). 
  • 프로퍼티는 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는 속성 소유자와 같거나 상위 유형이어야 합니다(확장 속성의 경우 확장되는 유형이어야 함). 
  • 프로퍼티는 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
        }
    }
}

getValue() 및/또는 setValue() 함수는 대리자 클래스의 멤버 함수 또는 확장 함수로 제공될 수 있습니다. 후자는 원래 이러한 기능을 제공하지 않는 객체에 속성을 위임해야 할 때 편리합니다. 두 함수 모두 operator 키워드로 표시해야 합니다.

 

참고 : 코틀린 공식문서

반응형