Jinhyuk Kim

Software Development Engineer @ Amazon

reniowood at gmail.com
resume

코틀린 공식 문서 번역 - 위임된 프로퍼티 (Delegated Properties)

2018-04-12

원본 문서: Delegated Properties - Kotlin Progamming Language


위임된 프로퍼티 (Delegated Properties)

매번 필요할 때마다 일일이 구현하지만, 한 번 구현해서 라이브러리에 넣으면 좋을만한 공통 프로퍼티가 존재한다. 예를 들면:

이런 일들을 하기 위해, 코틀린은 위임된 프로퍼티를 지원한다.

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

문법은 val/var <프로퍼티 이름>: <타입> by <구문>이다. by 뒤에 오는 구문은 해당 프로퍼티의 get()set()이 호출되면 각각 자신의 getValue()setValue()로 호출을 위임하는 대리자이다. 프로퍼티 대리자는 인터페이스를 구현할 필요는 없지만 getValue()setValue() 함수를 제공해야 한다.

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, '${property.name}'를 위임해줘서 고마워요!"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value 가 $thisRef 의 '${property.name}'에 할당되었습니다.")
    }
}

Delegate의 인스턴스에 위임한 p를 읽으면, DelegategetValue()가 호출되고, 이 함수의 첫 번째 파라미터(thisRef: Any?)는 p를 읽은 객체가 되고 두 번째 파라미터(property: KProperty<*>)는 p자체가 된다.

예를 들어,

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

위 코드는

Example@33a17727, 'p'를 위임해줘서 고마워요!

를 출력한다.

p에 값을 할당할 때도 마찬가지로, setValue() 함수가 호출된다. 첫 번째, 두 번째 파라미터는 getValue()와 같고 세번째 파라미터는 할당된 값을 가리킨다.

e.p = "NEW"

NEW 가 Example@33a17727의 'p'에 할당되었습니다.

를 출력한다.

위임된 객체를 위한 요구 사항 상세는 아래에서 찾을 수 있다.

코틀린 1.1부터는 위임된 프로퍼티은 클래스 멤버일 필요가 없으며, 함수나 코드 블럭 내에서 선언할 수 있다. 아래에서 예제를 살펴볼 수 있다.

표준 대리자

코틀린 표준 라이브러리는 대리자를 위한 몇 가지 유용한 팩토리 메서드를 제공한다.

Lazy

lazy()는 람다를 받아 지연 프로퍼티를 구현한 대리자인 Lazy<T>의 인스턴스를 반환한다. get()을 처음 호출하면 lazy()에 전달한 람다가 실행되고, 그 결과가 저장된다. 이후에 get()을 호출하면 저장한 값을 반환한다.

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

fun main(args: Array<String>) {
    println(lazyValue)
    println(lazyValue)
}

위 예제는

computed!
Hello
Hello

을 출력한다.

기본값으로 지연 프로퍼티의 평가는 동기적이다. 값은 하나의 스레드에서만 계산되고, 다른 모든 스레드에선 같은 값을 본다. 대리자 초기화를 동기화 할 필요가 없다면, 즉 여러 개의 스레드가 동시에 초기화를 해도 된다면 LazyThreadSafetyMode.PUBLICATIONlazy()의 파라미터로 넘긴다. 항상 하나의 스레드에서만 초기화가 일어난다고 확신한다면 다중 스레드 안전성을 보장하지 않는 대신 관련된 부하를 줄일 수 있는 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(args: Array<String>) {
    val user = User()
    user.name = "first"
    user.name = "second"
}

위 예제는

<no name> -> first
first -> second

를 출력한다.

할당을 가로채고 거부하고 싶다면, observable()대신 vetoable()을 사용하면 된다. vetoable의 핸들러는 할당 이전에 호출된다.

맵에 프로퍼티 저장하기

맵에 프로퍼티값을 저장하는 것도 위임된 프로퍼티의 흔한 예제다. 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) // "John Doe"를 출력한다.
println(user.age)  // 25를 출력한다.

Map대신 MutableMap을 이용하면 var인 프로퍼티도 사용할 수 있다.

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

지역 위임된 프로퍼티 (1.1부터)

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

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

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

memoizedFoo 변수는 처음 접근할 때 초기화된다. someCondition이 거짓이면 변수는 초기화되지 않을 것이다.

프로퍼티 대리자 요구 사항

읽기 전용 프로퍼티 (val) 을 위해 대리자는 다음 파라미터를 받는 getValue 함수를 제공해야 한다.

getValue 함수는 프로퍼티과 같은 타입(또는 프로퍼티의 서브타입)을 반환해야 한다.

변경 가능한 프로퍼티 (var) 을 위해 대리자는 다음 파라미터를 받는 setValue 함수를 추가적으로 제공해야 한다.

getValue()setValue() 함수는 대리자 클래스의 멤버 함수 또는 확장 함수로 제공되어야 한다. 후자의 경우 원래 해당 함수를 제공하지 않는 객체에 위임된 프로퍼티를 추가할 때 편리하다. 두 함수 모두 operator 단어를 붙여야한다.

대리자 클래스는 operator 메서드를 가진 ReadOnlyPropertyReadWriteProperty 를 구현하기도 한다. 두 인터페이스는 코틀린 표준 라이브러리에 선언되어있다.

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

번역 규칙

위임된 프로퍼티를 처리할 때 코틀린 컴파일러는 보조 프로퍼티를 만들어 위임한다. 예를 들어, prop 프로퍼티를 위해 숨겨진 프로퍼티인 prop$delegate을 만들어 이 프로퍼티에 위임한다.

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

코틀린 컴파일러는 생성한 메서드 인자에 prop의 모든 정보를 제공한다. 첫 번째 인자 this는 클래스 C의 인스턴스를 가리키고, this::propprop을 나타내는 KProperty 타입의 리플렉션 객체이다.

연결된 callable 참조 를 가리키는 this::prop 문법은 코틀린 1.1부터 지원한다.

대리자 제공 (1.1부터)

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)
        // 대리자를 만든다.
        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의 파라미터와 같다.

provideDelegate 메소드는 MyUI 인스턴스를 만드는 동안 매 프로퍼티마다 호출되고, 바로 필요한 검증을 실행한다.

프로퍼티과 대리자의 연결을 가로채는 능력이 없으면 같은 일을 하기 위해 불편하게도 프로퍼티 이름을 직접 전달해야한다.

// "provideDelegate" 기능 없이 프로퍼티 이름 확인하기
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)
   // 대리자를 만든다.
}

생성된 코드는 provideDelegate 메소드는 보조 프로퍼티인 prop$delegate를 초기화할 때 호출한다. provideDelegate가 없이 생성된 코드와 비교해보자.

class C {
    var prop: Type by MyDelegate()
}

// 'provideDelegate' 기능을 사용해 컴파일러가 만든 코드
class C {
    // 추가적인 대리자 프로퍼티를 만들기 위해 "provideDelegate"를 호출한다.
    private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
    val prop: Type
        get() = prop$delegate.getValue(this, this::prop)
}

provideDelegate 메소드가 보조 프로퍼티를 만드는 데만 영향을 끼치고 접근자 코드에는 영향을 미치지 않는 점에 주목하자.