Delegation in Kotlin

WindSekirun (wind.seo)·2022년 4월 26일
0

이 글은 기존 운영했던 WordPress 블로그인 PyxisPub: Development Life (pyxispub.uzuki.live) 에서 가져온 글 입니다. 모든 글을 가져오지는 않으며, 작성 시점과 현재 시점에는 차이가 많이 존재합니다.

작성 시점: 2018-10-12

위임(Delegation) 이란 하나의 인스턴스에서 다른 인스턴스로 권한을 부여하는 것이다. 즉, 클래스 간의 상속을 이용해 고정적 관계를 형성하는 것이 아닌 가변적으로 형성하여 상속 기능을 구현할 수 있는 대체제라고 표현할 수 있다. 흔히 이런 점이 위임을 강력한 기제로 작용하는 이유가 된다.

위임은 발신 객체를 수신 객체로 명시적으로 전달하는 것으로 이루어 질 수 있는데, 이를 명시적 위임(Explicitly Delegation) 이라 부르며 객체지향의 개념을 가지고 있는 언어를 사용하고 있을 경우에 쉽게 구현이 가능하다. 또는, 명시적으로 전달하지 않고 암시적으로 전달할 수 있는데 이 것을 암시적 위임(Implicitly Delegation) 으로 부른다.

글에서 사용할 언어인 Kotlin 은 일반적인 명시적 위임 말고도 언어적 기능으로 클래스 위임(Implementation by Delegation) 과 위임 프로퍼티(Delegated Properties) 을 지원하는데, 이 글에서는 명시적 위임과 클래스 위임, 위임 프로퍼티에 대해 각각 알아보고 어떤 목적엔 어떤 위임을 사용해야 하는지 알아보려 한다.

Explicitly Delegation

상기했듯이 명시적 위임은 발신 객체를 수신 객체로 명시적으로 전달하는 것을 의미한다. 이는 객체지향의 개념을 가지고 있는 언어라면 구현이 가능한 위임 패턴이다. 즉, 발신 객체를 수신 객체로 전달하는 과정에서 모든 과정이 코드로서 나타낸 경우를 의미한다. 다음은 명시적 위임의 예제이다.

interface Coffee {
    fun build()
}

open class CoffeeImpl : Coffee {
    override fun build() {
        println("Delivered!")
    }
}

class CoffeeMaker(private val coffee: Coffee) : Coffee {
    override fun build() {
        coffee.build()
    }
}

fun main(args: Array<String>) {
    println("Welcome to Pyxis Cafe!")
    println()

    val cappuccino = CoffeeImpl()
    val cappuccinoMaker = CoffeeMaker(cappuccino)
    cappuccino.build()
}

CoffeeCoffeeMaker란 클래스가 있다고 가정해본다.  Coffee 는 특정한 메서드를 가진 인터페이스로 여기에서는 만들어진 커피를 고객에게 전달하는 build() 메서드가 있다.

CoffeeMaker 클래스는 Coffee 클래스 자체를 파라미터로 가지고 있다. 즉, 여기서 CoffeeCoffeeMaker 사이에 위임 통로(Delegation link) 가 발생한다. build() 메서드에서는 받은 Coffee 파라미터의 build() 메서드를 실행해 고객에게 전달되게 한다.

마지막으로, 실제로 코드가 실행될  main 메서드에선 Coffee 의 새 인스턴스인 카푸치노(cappuccino)를 생성하고, CoffeeMaker 의 인스턴스를 만들어 build() 를 실행한다. 생성한 카푸치노 인스턴스를 CoffeeMaker 클래스의 생성자에 삽입하는데, 이 때 두 객체 간의 위임이 실행된다. 즉, Coffee 의 기능을 CoffeeMaker 에 위임하는 것이다.

실제로 실행해보면 다음과 같은 결과가 나온다.

Coffee 는 상속 가능한 클래스이므로 Coffee 를 상속하는 하위 클래스를 만들어서  CoffeeMaker 에 위임시킬 수 있다.

...

class Latte : CoffeeImpl() {
    override fun build() {
        println("Latte Delivered!")
    }
}

fun main(args: Array<String>) {
    ...

    val latte = Latte()
    val latteMaker = CoffeeMaker(latte)
    latteMaker.build()
}

상기했듯이 위임 패턴이 강력한 기제로 작용되는 이유는 Coffee 의 인스턴스나 하위 클래스를 수 없이 만들어도 CoffeeMaker 자체는 한 가지 형태만 필요하다는 것이다. 즉, 어떤 형태가 와도 유연하고 강력하게 대응할 수 있다.

특히, 상기 예제에서는 언어직 기능을 사용하지 않았기 때문에 객체지향 언어라면 모두 사용이 가능하다. 다만, 명시적 위임에 문제가 있다면 CoffeeMakerCoffee 의 모든 public 메서드를 구현하여 원래의 Coffee 메서드를 수동으로 실행해야 하므로 작성해야 되는 코드가 늘어난다는 것이다. (이를 객체지향에서는 Forwarding 라 부른다.)

이를 해결하기 위해 언어적으로 발신 객체를 수신 객체로 전달하는 과정을 작성할 수 있는 기능이 필요한데, 여기에서 나오는 것이 Kotlin 에서 지원하는 클래스 위임이다.

Implementation by Delegation

클래스 위임은 명시적 위임 방식으로 구현함에 있어 상용적인 코드를 제거할 수 있는 기능이다. 즉, 명시적 위임에서 CoffeeMakerCoffee 의 모든 public 메서드를 구현하여 원래의 Coffee 메서드를 수동으로 실행해주었다면, 클래스 위임에서는 이러한 작업을 컴파일러가 대신하여 작업하는 것이다.

클래스 위임은 기반이 되는 객체가 interface일 때, 인터페이스 이름 by 변수 이름으로 사용할 수 있다. 즉, 여기에서는 class CoffeeMaker(coffee: Coffee) : Coffee by coffeeCoffeeMaker 클래스를 구성할 수 있다. 실제로 컴파일 되었을 때에는 명시적 위임과 같이 CoffeeMaker 클래스가 Coffee 클래스를 구현하고 coffee 의 메서드를 실행시키는 코드를 확인할 수 있는데, 컴파일된 코드는 다음과 같다.

여기까지 내용을 정리하자면, 명시적 위임은 클래스를 기반으로 발신 객체를 수신 객체로 명시적으로 전달하는 것이다. 그리고 클래스 위임은 언어적 기능을 사용하여 전달하는 기능을 컴파일러에 위임한 것이다.

Delegated Properties

위임은 클래스에 기반해서 이루어지는 것 뿐만 아니라, 특정 변수를 대상으로 하여 기능을 위임하는 역할도 있다.  이러한 역할을 가진 역할을 위임 프로퍼티 라고 부른다.

예를 들어, 해당 속성이 변경되었을 때에 대한 알림을 주는 Delegates.observable() 나 변수에 처음 접근할 때에 주어진 함수로 변수를 초기화 하고, 다음부터 접근할 때에는 설정된 값을 반환하는 lazy() 가 있다.

위임 프로퍼티를 사용하기 위해서는 특정 조건을 만족해야 하는데, 그 조건은 다음과 같다.

  • Val를 대상으로 할 경우
    • getValue 메서드 구현 필요
    • operator fun getValue(thisRef: Any?, property: KProperty<*>): T
    • thisRef: 해당 속성의 소유자와 동일하거나 상위 타입이어야 함
    • property: kotlin.reflect 패키지에 있는 클래스이며, 해당 속성에 대한 정보를 가짐 (이름, 반환 타입 등)
  • Var를 대상으로 할 경우
    • getValue, setValue 메서드 구현 필요
    • operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
    • value: 변수에 설정될 값

위 조건을 만족하는 클래스를 만들었을 경우 val(var) 이름 : 타입 by 위임 클래스 이름 으로 사용이 가능하며, 만일 Delegation 이라는 클래스를 만들었다면 val name: String by Delegation() 으로 사용할 수 있다.

예제로, JVM 환경에서 GC가 발생되기 전 까지는 참조를 유지하고, 발생하면 회수되는 WeakReference 에 대해 만들었는데, 다음은 그 코드이다.

class WeakReferenceDelegation<T>(private var value: WeakReference<T?>) {

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T? {
        return value.get()
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
        this.value = WeakReference(value)
    }
}

fun <T> weak(value: T) = WeakReferenceDelegation(WeakReference(value))

그리고 사용법은 다음과 같다.

var coffee: Coffee? by weak(null)

fun main(args: Array<String>) {
    coffee = CoffeeImpl()
    coffee?.build()
}

기존에 WeakReference 를 사용하려면 해당 변수의 타입을 WeakReference<Coffee> 로 선언하고, 값을 설정할 때 coffee = new WeakReference<>(coffee) 로 설정했던 것에 대비해 위임 프로퍼티를 사용하면 변수 타입을 그대로 유지하고, 값을 설정하는 것 또한 일반적인 방법으로 할 수 있다.

단, 글을 작성하는 일자(2018-10-13) 기준으로 coffee 객체가 mutable 하여 non-null 로 cast 되지 않는 버그가 있는데, 이는 방법을 더 찾아봐야 될 것 같다. 예상하기로는 Kotlin 1.3 에 추가될 contract 로 해결될 것으로 보인다.

정리

이번 글에서는 Kotlin 에서 사용이 가능한 세 가지 위임에 대해 살펴보았다. 다소 예제가 부족한 느낌은 있지만 정리하면서 그동안 혼동이 있었던 사실에 대해 이해한 것 같았다.

profile
Android Developer @kakaobank

0개의 댓글