조용한 담장

코틀린(Kotlin) 클래스(Class) : 위임(Delegation) 본문

kotlin

코틀린(Kotlin) 클래스(Class) : 위임(Delegation)

iosroid 2020. 1. 2. 16:34

코틀린(kotlin) 클래스의 위임(delegation) 을 살펴보자.

원문 https://kotlinlang.org/docs/reference/delegation.html 을 보며 정리.

Implementation by Delegation

kotlin 은 상속의 대안이 되는 Delegation pattern 을 boilerplate code 없이 지원한다.

아래 예제에서, Derived 클래스는 자신의 모든 public 멤버들을 특정 오브젝트(BaseImpl)로 위임하여 Base 인터페이스를 구현할 수 있다.

interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

class Derived(b: Base) : Base by b

fun main() {
    val b = BaseImpl(10)
    Derived(b).print()
}

// output:
// 10

Derived 클래스의 supertype list 안에 쓰인 by 절 은 b 는 Derived 의 오브젝트들 안에 내부적으로 저장될 것이고 컴파일러는 b 로 보내는 Base 의 모든 메소드들을 생성할 것임을 나타낸다.

Overriding a member of an interface implemented by delegation

컴파일러는 위임 오브젝트의 것들 대신 override 된 것들을 사용하게 된다.

Derivedoverride fun printMessage() { print("abc") } 를 추가한다면 printMessage 를 호출 했을 때 "10" 대신 "abc" 를 출력하게 될것이다.

interface Base {
    fun printMessage()
    fun printMessageLine()
}

class BaseImpl(val x: Int) : Base {
    override fun printMessage() { print(x) }
    override fun printMessageLine() { println(x) }
}

class Derived(b: Base) : Base by b {
    override fun printMessage() { print("abc") }
}

fun main() {
    val b = BaseImpl(10)
    Derived(b).printMessage()
    Derived(b).printMessageLine()
}

// output:
// abc10

하지만 이렇게 override 된 멤버들은 위임 오브젝트의 멤버로부터 호출 되지 않는다.

위임 오브젝트의 멤버들은 인터페이스 멤버들의 구현들만 접근이 가능하다.

interface Base {
    val message: String
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override val message = "BaseImpl: x = $x"
    override fun print() { println(message) }
}

class Derived(b: Base) : Base by b {
    // This property is not accessed from b's implementation of `print`
    override val message = "Message of Derived"
}

fun main() {
    val b = BaseImpl(10)
    val derived = Derived(b)
    derived.print()
    println(derived.message)
}

// output:
// BaseImpl: x = 10
// Message of Derived

Delegated Properties

class Example {
    var p: String by Delegate()
}

위임 프로퍼티의 문법 : val/var <property name>: <Type> by <expression>

프로퍼티의 get(), set()getValue(), setValue() 로 위임된다.

프로퍼티 위임은 인터페이스를 갖지 않고 getValue() 와 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.")
    }
}

프로퍼티 p 를 읽으면 DelegategetValue() 가 호출되고, 값을 읽는 대상 오브젝트인 p 가 첫번째 파라미터가 되고 두번째 파라미터는 p 의 정보가 된다.

val e = Example()
println(e.p)

// outout:
// Example@33a17727, thank you for delegating ‘p’ to me!

p 로 값을 할당할 때는 setValue() 가 호출되고, 두개의 파라미터는 위와 같고 세번째 파라미터는 할당할 값이 된다.

e.p = "NEW"

// output:
// NEW has been assigned to ‘p’ in Example@33a17727.

함수나 코드블럭 안에 위임된 프로퍼티를 선언할 수 있기 때문에 반드시 클래스의 멤버가 될 필요는 없다.

Standard Delegates

kotlin standard library 가 제공하는 위임 메소드들 이다.

Lazy

lazy() 함수는 람다를 파라미터로 받고 lazy property 를 구현하기 위한 위임으로써의 기능을 제공하는 LAZY<T> 의 인스턴스를 리턴한다.

첫번째 get() 호출은 lazy() 로 전달된 람다를 실행하고 결과를 기억해두며, 후속으로 get() 이 호출되면 그 기억된 결과를 단순히 리턴 한다.

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

fun main() {
    println(lazyValue)
    println(lazyValue)
}

// output:
// computed!
// Hello
// Hello

기본적으로 lazy properties 의 evaluation (값의 판단) 은 동기화 된다.

값은 한개의 쓰레드에서만 처리되며 모든 쓰레드는 같은 값을 보게 된다.

초기화 위임(initialization delegate)의 동기화가 필요하지 않으면 멀티 쓰레드는 동시에 실행할 수 있고 LazyThreadSafetyMode.PUBLICATIONlazy() 의 파라미터로 전달한다.

초기화는 항상 property 를 쓰는 한개의 쓰레드에서만 일어난다고 하면 LazyThreadSafetyMode.NONE 를 쓸 수 있다.

그러면 쓰레드 안전을 보장하며 관련된 오버헤드를 발생하지 않는다.

Observable

Delegates.observable() 는 초기값과 수정을 위한 핸들러를 인자로 받는다.

핸들러는 property 에 값을 할당할 때 마다 호출되며 값이 할당 될 property, 이전값과 새로운 값 세개의 파라미터를 갖는다.

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"
}

// output:
// <no name> -> first
// first -> second

할당 값을 중간에 가로채어 조건에 따라 할당 동작을 막고 싶다면 vetoable() 을 쓰면 된다.

핸들러는 새로운 property 값의 할당 전에 실행 된다.

Storing Properties in a Map

map 인스턴스 자신을 위임된 프로퍼티를 위한 위임으로 쓸 수 있다.
위임된 프로퍼티는 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) // Prints "John Doe"
    println(user.age)  // Prints 25
}

read-only Map 대신 MutableMap 을 쓰면 var 프로퍼티에도 쓸 수 있다.

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

Local Delegated Properties

지역 변수를 위임된 프로퍼티로 선언할 수 있다.

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

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

memoizedFoo 변수는 처음 사용될 때만 처리된다.

someCondition 이 false 이면 변수는 처리되지 않을 것이다.

Property Delegate Requirements

read-only property val 의 경우, 위임은 아래의 파라미터를 가지는 getValue 이름의 함수를 제공해야만 한다.

  • thisRef : property owner 의 supertype 이거나 같아야 한다.
  • property : KProperty<*> 타입 이거나 이것의 supertype 이어야 한다.

이 함수는 property 와 같은 타입이나 subtype 을 리턴해야 한다.

mutual property var 의 경우, 위임은 아래의 파라미터를 가지는 setValue 이름의 함수를 추가로 제공해야 한다.

  • thisRef : getValue() 와 같다.
  • property : getValue() 와 같다.
  • new value : property 와 같은 type 이거나 subtype 이어야 한다.

getValue(), setValue() 함수는 위임 클래스나 확장 함수의 멤버 함수로 제공될 수 있다.

후자의 것은 이런 함수를 제공하지 않는 오브젝트에 프로퍼티를 위임할 필요가 있을 때 유용하다.

두 함수 모두 operator 키워드로 표시되어야 한다.

위임 클래스는 필요한 operator 메소드를 가진 ReadOnlyPropertyReadWritePRoperty 인터페이스중 하나를 구현해야 한다.

이 인터페이스들은 kotlin standard library 에 선언되어 있다.

interface ReadOnlyProperty<in R, out T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
}

interface ReadWriteProperty<in R, T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
    operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}

Translation Rules

위임된 프로퍼티를 위해 코틀린 컴파일러는 먼저 보조 프로퍼티를 생성한 후 그것에 위임을 한다.

아래 예제 코드의 동작을 보면, prop 프로퍼티를 위해 prop$delegate 라는 숨겨진 프로퍼티가 생성되고 접근자들(get()/set())의 코드에서 단순히 이 추가된 프로퍼티로 위임을 한다.

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)
}

코틀린 컴파일러는 arguments 에 있는 prop 에 관한 모든 필요한 정보를 제공한다.

첫번째 argument this 는 클래스 C 의 인스턴스를 참조하고 this::propprop 자신을 묘사하는 KProperty 타입의 reflection object 이다.

코드에서 바로 bound callable reference 을 참조하기 위한 this::prop 문법은 코틀린 1.1 이후에 적용되어 있다.

Providing a delegate (since 1.1)

provideDelegate operator 를 정의 함으로써 오브젝트 생성의 로직을 구현이 위임된 프로퍼티로 확장할 수 있다.

오브젝트가 by 의 오른쪽에서 사용되면, provideDelegate 를 멤버나 확장 함수로써 정의 하고 그 함수는 프로퍼티 위임 인스턴스(property delegate instance) 를 생성하기 위해 호출된다.

사용의 예제로는 getter, setter 나 그외에서도 프로퍼티가 생성됬을때 프로퍼티의 일관성(consistency) 을 체크하는데 사용하는 경우가 있다.
바인딩(binding) 전에 프로퍼티의 이름을 체크하려면 아래의 예제처럼 할 수 있다.

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 owner 의 supertype 이거나 같아야 한다.
  • property : KProperty<*> 타입 이거나 이것의 supertype 이어야 한다.

provideDelegate 메소드는 MyUI 인스턴스를 생성할 때 각각의 프로퍼티에 대해 호출되고, 필요한 확인(validation) 이 바로 수행된다.

프로퍼티와 위임 사이의 바인딩을 가로채는 것 없이 같은 기능을 구현하려면 명확하기 프로퍼티의 이름을 전달해줘야 하며 이는 매우 불편한 방법이다.

// 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() 을 위해 새성된 코드를 비교해 보자.

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)
}
provideDeleg

provideDelegate 메소드는 보조 프로퍼티의 생성에만 영향을 주며 getter, setter 의 생성된 코드에는 영향을 주지 않는다.

Comments