Software Development Engineer @ Amazon
reniowood at gmail.com resume
원본 문서: Delegated Properties - Kotlin Progamming Language
매번 필요할 때마다 일일이 구현하지만, 한 번 구현해서 라이브러리에 넣으면 좋을만한 공통 프로퍼티가 존재한다. 예를 들면:
이런 일들을 하기 위해, 코틀린은 위임된 프로퍼티를 지원한다.
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
를 읽으면, Delegate
의 getValue()
가 호출되고, 이 함수의 첫 번째 파라미터(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<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.PUBLICATION
를 lazy()
의 파라미터로 넘긴다. 항상 하나의 스레드에서만 초기화가 일어난다고 확신한다면 다중 스레드 안전성을 보장하지 않는 대신 관련된 부하를 줄일 수 있는 LazyThreadSafetyMode.NONE
를 사용할 수 있다.
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
}
지역 변수를 위임된 프로퍼티으로 선언할 수 있다. 예를 들어, 지역 변수를 lazy로 만들 수 있다.
fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)
if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}
memoizedFoo
변수는 처음 접근할 때 초기화된다. someCondition
이 거짓이면 변수는 초기화되지 않을 것이다.
읽기 전용 프로퍼티 (val
) 을 위해 대리자는 다음 파라미터를 받는 getValue
함수를 제공해야 한다.
thisRef
- 프로퍼티 주인와 같은 타입 혹은 슈퍼타입이어야 한다. (확장 프로퍼티의 경우 확장된 타입)property
- KProperty<*>
거나 슈퍼타입이어야 한다.getValue
함수는 프로퍼티과 같은 타입(또는 프로퍼티의 서브타입)을 반환해야 한다.
변경 가능한 프로퍼티 (var
) 을 위해 대리자는 다음 파라미터를 받는 setValue
함수를 추가적으로 제공해야 한다.
thisRef
- getValue()
와 같다.property
- getValue()
와 같다.getValue()
와 setValue()
함수는 대리자 클래스의 멤버 함수 또는 확장 함수로 제공되어야 한다. 후자의 경우 원래 해당 함수를 제공하지 않는 객체에 위임된 프로퍼티를 추가할 때 편리하다. 두 함수 모두 operator
단어를 붙여야한다.
대리자 클래스는 operator
메서드를 가진 ReadOnlyProperty
나 ReadWriteProperty
를 구현하기도 한다. 두 인터페이스는 코틀린 표준 라이브러리에 선언되어있다.
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::prop
은 prop
을 나타내는 KProperty
타입의 리플렉션 객체이다.
연결된 callable 참조 를 가리키는 this::prop
문법은 코틀린 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
의 파라미터와 같다.
thisRef
- 프로퍼티 주인와 같은 타입 혹은 슈퍼타입이어야 한다. (확장 프로퍼티의 경우 확장된 타입)property
- KProperty<*>
거나 슈퍼타입이어야 한다.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
메소드가 보조 프로퍼티를 만드는 데만 영향을 끼치고 접근자 코드에는 영향을 미치지 않는 점에 주목하자.