Language/Kotlin

[Kotlin] Delegation (위임)

Junhong Kim 2023. 6. 18. 23:03
728x90
반응형

Delegation 이란?

Delegation(위임)은 클래스나 프로퍼티의 기능을 다른 객체에 위임하는 디자인 패턴입니다. Kotlin에서는 보일러 플레이트 코드 없이 `by` 키워드를 사용해서 `Delegation Pattern`을 구현할 수 있는 방법을 지원합니다.

클래스 위임

클래스 위임은 특정 인스턴스의 메서드 호출시 해당 기능을 다른 클래스에게 위임합니다. 다음 예제는 `Child` 클래스의 `pay`를 호출하면 `Parent` 클래스의 `pay` 메서드를 실행합니다. 이때 `Child` 클래스 선언에 `by` 키워드를 사용했으며, `Parent` 클래스의 인스턴스를 생성자 파라미터로 전달합니다. 이제 `Child` 클래스가 `pay` 기능을 사용하면 `Parent` 클래스에게 위임하여 `pay` 메서드를 호출합니다.

일반적인 클래스 위임

interface Adult {
    fun pay()
}

class Parent : Adult {
    override fun pay() {
        println("Paying bills by parent")
    }
}

class Child(private val parent: Parent) : Adult {
    override fun pay() {
        parent.pay() // parent의 pay 기능을 사용한다
    }
}

fun main() {
    val parent = Parent()
    val child = Child(parent)
    child.pay()
}

---

Paying bills by parent

 

`by`를 사용한 클래스 위임

interface Adult {
    fun pay()
}

class Parent : Adult {
    override fun pay() {
        println("Paying bills by parent")
    }
}

class Child(private val parent: Parent) : Adult by parent // Adult 기능을 모두 Parent에게 위임한다.

// [참고] 다음과 같은 방식으로도 위임할 수 있다.
// class Child : Adult by Parent()

fun main() {
    val parent = Parent()
    val child = Child(parent)
    child.pay()
}

---

Paying bills by parent

Delegated Properties (위임 프로퍼티)

앞서 살펴본 `Delegation Pattern`을 프로퍼티에 적용해서 접근자(accessor) 기능을 위임 객체가 수행할 수 있도록 위임하는 방법을 알아봅니다. 위임 프로퍼티의 문법은 다음과 같으며, `p` 프로퍼티는 접근자 로직을 다른 객체에 위임한다는 것을 의미합니다.

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

프로퍼티 위임 관례를 따르기 위해서는 위임 클래스(Delegate)에 `getValue`와 `setValue` 메서드를 제공해야 합니다. 이때, `operator` 키워드를 사용하여 getter/setter 로직을 구현한다는 것에 유의해주세요.

class Delegate {
    // getter
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }
    // setter
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name}' in $thisRef.")
    }
}

Delegate 구현에 대한 내용을 확인했으니 Example 클래스의 접근자 기능을 사용해보면, 다음과 같이 Delegate의 접근자 로직이 수행된 것을 확인할 수 있습니다.

fun main() {
val e = Example()
    println(e.p)
    e.p = "NEW"
}

---

delegate.Example@614c5515, thank you for delegating 'p' to me!
NEW has been assigned to 'p' in delegate.Example@614c5515.

Standard delegates

Lazy Properties (지연 초기화)

지연 초기화는 객체의 일부분을 초기화하지 않고 실제 그 값이 필요할 때 초기화하는 패턴입니다. 초기화 과정에 많은 자원을 사용하거나 객체를 사용할 때마다 초기화하지 않아도 되는 프로퍼티에 대해 지연 초기화 패턴을 사용할 수 있습니다. 예를 들어, 특정 프로퍼티를 초기화하기 위해서 많은 연산 또는 시간이 오래 걸릴 경우 초기화를 뒤로 미룰 수 있습니다.

`backing property`를 사용한 지연 초기화 구현

class Person(val name: String) {
    private var _emails: List<String>? = null
    val emails: List<String>
        get() {
            if (_emails == null) {
                _emails = loadEmails(this) // 최초 접근시 이메일 load
            }
            return _emails!!
        }
}

fun loadEmails(person: Person): List<String> {
    println("Load emails for ${person.name}")
    return listOf("email1", "email2")
}

fun main() {
    val person = Person("Kim")
    println(person.emails)
    println(person.emails)
}

---

Load emails for Kim
[email1, email2]
[email1, email2]

위임 프로퍼티를 사용한 지연 초기화 구현

`backing property`를 사용한 지연 초기화는 많은 코드를 작성해야하고 스레드 세이프하지 않습니다. 코틀린에서는 위임 프로퍼티를 사용해서 코드를 간단하게 할 수 있으며 getter 로직을 캡슐화 해줍니다. 위임 객체를 반환하는 표준 라이브러리 `lazy` 함수는 getValue 메서드가 있는 객체를 반환합니다. 따라서, `lazy`를 `by` 키워드와 함꼐 사용해서 위임 프로퍼티를 만들 수 있습니다.

class Person(val name: String) {
    val emails by lazy { loadEmails(this) }
}

fun loadEmails(person: Person): List<String> {
    println("Load emails for ${person.name}")
    return listOf("email1", "email2")
}

fun main() {
    val person = Person("Kim")
    println(person.emails)
    println(person.emails)
}

---

Load emails for Kim
[email1, email2]
[email1, email2]

 

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에 전달된 핸들러는 새 프로퍼티 값을 할당하기 전에 호출됩니다.

참고

https://kotlinlang.org/docs/delegation.html

https://in-kotlin.com/design-patterns/interface-and-property-delegation/#Override_Interface_members

https://product.kyobobook.co.kr/detail/S000001804588

728x90
반응형