Kotlin Delegation을 알아보고, Delegated Properties와 class에서의 Delegation을 알아본다.



개인 광고 영역

Kotlin은 Delegation을 제공하고, 매우 유용하게 사용할 수 있다.

매우 흔하게 볼 수 있는 by lazy {}를 통해 Delegated Properties을 알아보고, class에서 활용할 수 있는 Delegation을 알아본다.


이 글에서 알아볼 내용

  • Delegation을 알아본다.
  • Delegated Properties를 알아본다.
  • Lazy를 사용하는 property를 알아본다.
  • class에서 간단하게 사용할 수 있는 Delegation을 알아본다.


Delegation 자료

2019년 한국 코틀린 행사에서 Pluu님이 발표했던 Kotlin을 여행하는 히치하이커의 준비서를 보면 자세하게 설명하고 있으니 참고하시면 좋다.


Delegation 이란?

Stack Overflow의 문서 What does ‘by’ keyword do in Kotlin? - StackOverflow에 잘 나와있는데, 크게 두 가지를 정리하고 있다.

  • by 키워드를 활용한 Properties에서의 활용
  • interface를 class delegation에서의 활용


by 키워드를 활용한 Properties에서의 활용

코틀린을 오래 사용한 사람들은 이미 by lazy {}를 익숙하게 사용한다.

private val viewModel: MainViewModel by lazy {
    MainViewModel()
}

lazy의 기본 코드는 아래와 같다. lazy의 자세한 사용법은 별도의 글을 참고하길.

public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

결국 by 이후에 오는 lazy에게 프로퍼티 생성을 위임하고, lazy의 내부 동작에 따라 코드를 초기화한다.

Lazy의 최상위는 interface로 구성되어 있고, property인 value와 함수인 isInitialized로 구성되어 있다. 결국 value는 Properties를 getter로 구성해 값을 리턴하는데, 이때 lazy 패턴을 활용하는 형태로 구성되어 있다.

/**
 * Represents a value with lazy initialization.
 *
 * To create an instance of [Lazy] use the [lazy] function.
 */
public interface Lazy<out T> {
    /**
     * Gets the lazily initialized value of the current Lazy instance.
     * Once the value was initialized it must not change during the rest of lifetime of this Lazy instance.
     */
    public val value: T

    /**
     * Returns `true` if a value for this Lazy instance has been already initialized, and `false` otherwise.
     * Once this function has returned `true` it stays `true` for the rest of lifetime of this Lazy instance.
     */
    public fun isInitialized(): Boolean
}

참고로 SynchronizedLazyImpl는 by lazy {}를 사용하면 가장 기본으로 생성되고, 사용하는 코드인데 아래와 같다. 내부적으로 값이 호출되기 전에는 temp 값을 담을 수 있도록 만들고, 외부에서 value를 호출하면 value 안에 있는 get()에서 이를 늦은 처리하도록 한다.

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {

    // 생략

    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    // 생략
}

결국 by lazy {} 호출 시 lazy에게 위임해 내부 코드의 동작에 따라 delegation 처리를 함을 알 수 있다.


delegation by viewModels 도 살짝 살펴보자.

위임 패턴 viewModels도 하나 더 알아보자.

Fragment ktx를 사용하면 by viewModels를 사용할 수 있다.

implementation "androidx.fragment:fragment-ktx:1.2.5"

이 역시 by 키워드를 통해 delegation을 쉽게 사용할 수 있다.

private val viewModel by viewModels<MainViewModel>()

앞에서는 lazy를 활용해 늦은 초기화를 했었는데, viewModels 역시 늦은 초기화는 내부에 lazy를 활용하고 있다. 먼저 viewModels는 아래와 같이 생겼고, 여기에서도 보면 알겠지만 createViewModelLazy 함수를 호출한다.

@MainThread
inline fun <reified VM : ViewModel> Fragment.viewModels(
    noinline ownerProducer: () -> ViewModelStoreOwner = { this },
    noinline factoryProducer: (() -> Factory)? = null
) = createViewModelLazy(VM::class, { ownerProducer().viewModelStore }, factoryProducer)

여기에서 한 단계 더 들어가 보면 이 역시 Lazy<>를 활용함을 알 수 있다.

@MainThread
fun <VM : ViewModel> Fragment.createViewModelLazy(
    viewModelClass: KClass<VM>,
    storeProducer: () -> ViewModelStore,
    factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
    val factoryPromise = factoryProducer ?: {
        defaultViewModelProviderFactory
    }
    return ViewModelLazy(viewModelClass, storeProducer, factoryPromise)
}

이 코드에서도 Lazy를 상속받아 구현되어 있어 결과적으론 by lazy와 동일한 결과를 볼 수 있다. 결국 호출하기 전에는 생성하지 않는 형태를 감싸고, 이를 delegation을 이용해 쉽게 사용할 수 있다.


Delegated Properties

결국 이 코드들은 모두 Delegated Properties 활용법에 속한다.

kotlin.properties.ReadOnlyProperty, kotlin.properties.ReadWriteProperty 두 개를 각각 상속받아 property 활용이 가능하다.

그중 내부 Delegates에 포함되어 있는 코드는 아래와 같다.

private class NotNullVar<T : Any>() : ReadWriteProperty<Any?, T> {
    private var value: T? = null

    public override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return value ?: throw IllegalStateException("Property ${property.name} should be initialized before get.")
    }

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

ReadWriteProperty를 상속받아 구현하고 외부에서 값을 주입하고, 변경할 수 있다.

현 코드에서는 별도의 동선이 없지 만 특정 값을 체크하거나, 안전하게 사용할 필요성이 있다면 이러한 delegated 생성을 활용할 수 있다.

fun resourceDelegate(): ReadWriteProperty<Any?, Int> =
    object : ReadWriteProperty<Any?, Int> {
        var curValue = 0
        override fun getValue(thisRef: Any?, property: KProperty<*>): Int = curValue
        override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
            curValue = value
        }
    }

private var resource: Int by resourceDelegate()

// 사용은 이렇게
resource = R.string.app_name
android.util.Log.d("TEMP", "resource $resource")


Delegated Properties 만들 시 주의사항

findViewById를 쉽게 만들기 위해서 delegated를 생성할 수 있다.

그래서 생성한 코드는 아래와 같다.

fun <T> findView(view: View?, resourceId: Int): Lazy<T> = object : Lazy<T> {

    private var _view: T? = null

    override val value: T
        get() {
            if (_view == null) {
                _view = view?.findViewById(resourceId)
            }
            return _view!!
        }

    override fun isInitialized(): Boolean =
        _view != null
}

private val message: TextView by findView(view, R.id.tv_message)

하지만 문제가 있다. findView를 호출하는 시점에 아래 코드는 호출된다.

_view = view?.findViewById(resourceId)

다만 findView를 만들어지는 시점에 view를 미리 넘기기 때문에 이미 null인 상태의 값이 넘어가 getValue에서 활용한다. 결국 항상 null 일 수밖에 없다.

이런 패턴에서의 주의할 점은 lazy 내부에서 처리하는 것과, 값을 초기화하는 것을 구분해야 한다. 그러다 보니 by lazy 쪽 패턴을 보면 알 수 있는데 Higher-Order function을 활용해 getValue 호출 시점에 외부의 값을 넘겨줘 처리함을 알 수 있다.

이 경우는 그냥 by lazy를 활용하는 게 가장 안정적이고, 작성하는 코드를 줄일 수 있다.

private val message by lazy {
    requireView().findViewById<TextView>(R.id.message)
}


interface를 class delegation에서의 활용

상속 대신 delegation을 활용할 수 있다. 이 역시 by 키워드를 활용해 처리할 수 있다.

이렇게 외부에서 주입을 통해 Base를 갈아치울 수도 있고,

interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

class Derived(b: Base) : Base by b

fun main() {
    val b = BaseImpl(10)
    Derived(b).print()
}

바로 초기화해 사용하는 것도 가능하다.

interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

class Derived : Base by BaseImpl()

fun main() {
    Derived().print()
}

클래스에서 by delegation을 사용하는 건 매우 간단하고, 상황에 따라 알맞게 사용하면 되겠다.

이 경우 마치 원래 있던 함수처럼 외부와 내부 모두에서 사용하는 게 가능하다.

그래서 아래와 같이 Derived class 내부에서도 별도의 처리 없이 바로 print() 함수 호출이 가능하다.

class Derived : Base by BaseImpl() {

  fun test() {
    print()
  }
}


마무리

코틀린의 by는 위임을 할 수 있는 키워드이다. 위임을 통해 Property와 class에서의 interface를 통해 위임을 간단하게 활용하는 게 가능하다.

이번 글 요약

  • Kotlin의 by 키워드 활용법을 알아보았다.
  • 위임을 위해 만들어지는 Property 정의는 ReadOnlyProperty, ReadWriteProperty를 이용할 수 있다.
  • Lazy 패턴을 상속받아 늦은 초기화 위임을 만들 수 있지만, Parameter 사용 시에는 객체 생성과 같기 때문에 View와 같은 라이프 사이클이 필요한 경우 주의해 사용해야 한다.
  • class에서의 위임은 interface 정의만 잘 하면 매우 유용하게 활용 가능하다.


About Taehwan

My name is Taehwan Kwon. I have developed Android for 6 years and blog has been active for eight years.

Comments