위임 패턴의 정의
어떤 객체의 조작 일부를 다른 객체에게 넘김
위탁자(delegator) → 수탁자(delegate)
책임을 다른 클래스, 메서드에 떠넘기는 것을 말한다.
클래스 위임
이러한 위임 패턴을 코틀린에선 보일러 플레이트 없이 제공한다.
interface Base { //수탁자 delegate
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() {
print(x)
}
}
class Derived(b: Base) : Base by b //위탁자 delegator
fun main(args: Array<String>){
val b = BaseImpl(10)
Derived(b).print()
}
출력결과
10
Derived의 상위 타입에 있는 by 절은 Derived의 객체 내부에 b를 저장한다는 의미다. 컴파일러는 Base의 모든 메서드를 b로 전달하도록 Derived를 생성한다.
override는 그대로 동작한다
interface Base { //수탁자 delegate
fun print()
}
class BaseImpl(val x: Int) : Base { //수탁자 delegate 대리객체
override fun print() {
print(x)
}
}
class Derived(b: Base) : Base by b { //위탁자 delegator
override fun print() {
print("abc")
}
}
fun main(args: Array<String>) {
val b = BaseImpl(10)
Derived(b).print()
}
출력결과
abc
이러한 방식은 상속과 다르게 Derived가 Base인터페이스를 상속받는 인스턴스를 포함하도록 하고 Derived가 받은 요청을 포함하는(위 예제에선 BaseImpl) 인스턴스로 전달하는 것이다.
위임 프로퍼티
3가지가 있다.
- lazy 프로퍼티 : 처음 접근할 때 값을 계산
- observable 프로퍼티 : 프로퍼티가 바뀔 때 리스너에 통지
- 맵에 저장할 프로퍼티 : 각 프로퍼티를 별도 필드에 저장하는 대신에 맵에 저장
위임 프로퍼티는 다음과 같이 쓸 수 있다.
class Example {
var p: String by Delegate()
}
구문은 val/var <property name>: <Type> by <expression>
이다. by뒤에 오는게 대리 객체이고 프로퍼티에 getValue(), setValue() 메서드를 위임한다.
프로퍼티 대리객체는 인터페이스를 구현해선 안되며 getValue()와 setValue()를 제공해야 한다.(var 일경 우만 setValue() 제공)
fun main() {
println( Example().p)
Example().p = "new"
}
class Example{
val p: String by Delegate()
}
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")
}
}
출력결과
Example@4667ae56, thank you for delegating 'p' to me!
new has been assigned to 'p' in Example@77cd7a0
위 예제는 프로퍼티 p를 읽을 때 위임한 Delegate인스턴스의 getValue()를 호출한다. 쓸때는 setValue()를 호출한다.
thisRef는 p를 포함한 객체를 나타내는 것이고, property는 프로퍼티 p 자체에 대한 설명을 포함한다.
표준 위임
lazy
람다로 파라미터를 받고 Lazy<T>
인스턴스를 리턴하는 함수이다. get()을 처음 호출할 때 람다를 실행하고 결과를 기억한다. 이후 호출은 단순히 결과를 리턴해준다.
val lazyValue: String by lazy{
println("computed")
"Hello"
}
fun main(){
println(lazyValue)
println(lazyValue)
}
출력결과
---
computed // 실행
Hello // 결과 저장
Hello // 저장된 결과 호출
기본적으로 lazy프로퍼티의 평가를 동기화한다. 한 스레드에서만 값을 계산하고, 모든 스레드는 같은 값을 사용한다. 동기화가 필요 없다면 lazy() 함수에 패러미터로 LazyThreadSafeyMode.PUBLICATION
을 전달해서 동시에 여러 쓰레드가 초기화를 실행할 수 있게 허용할 수 있다.
단일 쓰레드가 초기화를 할 거라 확신할 수 없다면 LazyThreadSafetyMode.NONE
를 사용한다. 이 스레드는 안정성을 보장하지 않으며 관련 부하를 발생하지 않는다.
observable 프로퍼티
옵저버 패턴의 특징을 가지고 있는 프로퍼티이다.
두 개의 인자를 갖는다. 첫 번째는 초기값이고 두 번째는 수정에 대한 핸들러이다. 핸들러는 할당된 프로퍼티, 이전 값, 새 값의 세 패러미터를 갖는다.
class User{
var name: String by Delegates.observable("test"){
prop, old, new -> println("$old -> $new")
}
}
fun main() {
val user = User()
user.name = "First"
user.name = "Second"
}
출력결과
---
test -> First
First -> Second
만약 할당을 가로채서 거부하고 싶다면 vetoable()를 사용한다. 프로퍼티에 새 값을 할당하기 전에 vetoable에 전달할 핸들러를 호출한다.
맵에 저장할 프로퍼티
위임 프로퍼티로 맵에 프로퍼티 값을 저장할 수 있다.
class User(val map: Map<String, Any?>){
val name: String by map
val test: Int by map
}
fun main() {
val user = User(mapOf("name" to "test", "test" to 10))
println(user.name)
println(user.test)
}
출력결과
---
test
10
변수 명을 키로 삼는 값을 가져온다. 심플하면서도 간단하면서도 강력한 기능이다. JSON을 파싱하거나 다른 "동적인" 것을 하는 애플리케이션에 주로 사용한다.
읽기 전용 Map 대신에 MutableMap을 사용하면 var 프로퍼티에 동작한다.
class User(val map: MutableMap<String, Any?>){
var name: String by map
var test: Int by map
}
로컬 위임 프로퍼티
이러한 위임을 로컬 변수에도 할 수 있다.
fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)
if(someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}
memoizedFoo 변수에 처음 접근할 때만 계산한다.
프로퍼티 위임 요구사항
대리 객체에 대한 요구사항은 다음과 같다.
읽기 전용(val)
- 위임 객체의 이름은 getValue이고 다음 패러미터를 같은 함수를 제공해야 한다.
- thisRef : 프로퍼티 소유자와 같거나 또는 상위 타입이어야 한다(확장 프로퍼티의 경우 확장한 타입)
- property : KProperty <*> 타입 또는 그 상위 타입이어야 한다.
쓰기 전용(var)
- 위임 객체의 이름은 setValue이고 다음 패러미터를 같은 함수를 제공해야 한다.
- thisRef : 위와 동일
- property : 위와 동일
- value(새 값) : 프로퍼티와 같은 타입 또는 상위 타입이 여야 한다.
두 함수 모두 operator 키워드를 붙여야 하며 위임 클래스의 멤버 함수나 확장 함수로도 제공할 수 있다.
위임 클래스는 ReadOnlyProperty와 ReadWriteProperty 인터페이스 중 하나를 구현할 수 있다. 이 두 인터페이스 모두 표준 라이브러리에 구현돼있다.
public fun interface ReadOnlyProperty<in T, out V> {
public operator fun getValue(thisRef: T, property: KProperty<*>): V
}
public interface ReadWriteProperty<in T, V> : ReadOnlyProperty<T, V> {
public override operator fun getValue(thisRef: T, property: KProperty<*>): V
public operator fun setValue(thisRef: T, property: KProperty<*>, value: V)
}
변환 규칙
위임 프로퍼티를 컴파일러 입장에서 보면 재밌어진다. 코틀린 컴파일러는 private 한 보조 프로퍼티를 생성하고 그 프로퍼티에 위임한다.
class C {
var prop: Type by MyDelegate()
}
//in compiler
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)
}
this::prop는 prop자체를 설명하는 KProperty타입의 리플렉션 객체이다.
위임 객체 제공 (완벽히 이해를 못 했기 때문에 그대로 타이핑)
by의 오른쪽에서 사용할 객체가 provideDelegate를 멤버 함수나 확장 함수로 정의하면, 프로퍼티의 위임 대상 인스턴스를 생성할 때 그 함수를 호출한다.
provideDelegate의 가능한 한 가지 용도는 프로퍼티의 getter나 setter뿐만 아니라 생성할 때 프로퍼티의 일관성을 검사하는 것이다.
예를 들어, 값을 연결하기 전에 프로퍼티 이름을 검사하고 싶다면, 다음 코드로 이를 처리할 수 있다.
class ResourceLoader<T>(id: ResourceID<T>) {
operator fun provideDelegate(
thisRef: MyUI,
prop: KProperty<*>
): ReadOnlyProperty<MyUI, T> {
checkProperty(thisRef, prop.name)
}
private fun checkProperty(thisRef: MyUI, name: String) {...}
}
fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { ... }
class MyUI {
val image by bindResource(ResourceId.image_id)
val text by bindResource(ResourceId.text_id)
}
provideDelegate 파라미터는 getValue와 동일하다.
MyUI인스턴스를 생성하는 동안 각 프로퍼티에 대해 provideDelegate메서드를 호출하고 즉시 필요한 검증을 수행한다. provideDelegate메서드는 보조 프로퍼티의 생성 에만 영향을 주고, getter나 setter를 위한 코드 생성에는 영향을 주지 않는다.
'코틀린' 카테고리의 다른 글
16. 코루틴 기본 (0) | 2022.06.13 |
---|---|
15. 인라인 함수 (0) | 2022.06.13 |
14. 고차함수와 람다 (0) | 2022.06.12 |
13. 함수 (0) | 2022.06.12 |
11. 오브젝트 식과 선언 (0) | 2022.06.12 |