Kotlin ‘by’ 키워드

2 minute read

안드로이드 코드를 짜면서 by lazy, by viewModels()를 쓸 때 by라는 키워드를 사용했는데 이것이 어떤 역할을 하는지 작성해보려고 한다.

위임 패턴

객체가 요청을 다른 객체(helper object)에 위임해서 처리하는 패턴이다.

class Rectangle(val width: Int, val height: Int) {
    fun area() = width * height
}

class Window(val bounds: Rectangle) {
    //위임
    fun area() = bounds.area()
}

여기서 Window 클래스는 area 메서드로 호출되는 요청을 인자로 갖고 있는 Rectangle 객체에 위임하고 있다.

클래스 위임

interface ClosedShape {
    fun area(): Int
}

class Rectangle(val width: Int, val height: Int) : ClosedShape {
    override fun area() = width * height
}

class Circle(val radius: Int) : ClosedShape {
    override fun area() = radius * radius * 3
}

class Window(private val bounds: ClosedShape) : ClosedShape by bounds

fun main() {
    val circleWindow = Window(Circle(5))
    val rectangleWindow = Window(Rectangle(4, 5))
    println(circleWindow.area())        //prints 75
    println(rectangleWindow.area())     //prints 20
}

코틀린에서는 by 키워드를 사용하여 언어 차원에서 위임 패턴을 구현할 수 있다. Window 클래스는 ClosedShape를 구현하며, by 키워드로 인해 인자로 전달된 bounds가 구현한 override 메서드들을 사용할 수 있게 된다.

프로퍼티 위임

import kotlin.reflect.KProperty

class Example {
    var 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.")
    }
}

프로퍼티 위임은 프로퍼티의 Accessor(get, set)을 delegate에 위임하는 것이다. 위의 코드에서 프로퍼티 p: StringDelegategetValuesetValue를 이임하고, Delegate 클래스 내에 각각 오버로드돼있다.

val example = Example()
println(example.p) // prints Example@33a17727, thank you for delegating 'p' to me!
example.p = "NEW"  // prints NEW has been assigned to 'p' in Example@33a17727.

p를 읽으면 getDelegate에 위임돼있으므로 getValue 메서드가 호출된다. p를 수정하면 setDelegate에 위임돼있으므로 setValue 메서드가 호출된다. 각각 내부의 String이 리턴돼 위 코드의 주석처럼 출력된다.

by lazy

lazy() 메서드는 람다를 인자로 받아 Lazy<T> 인스턴스를 반환한다. get()의 첫 호출은 lazy()에 전달된 람다를 기억하고 그 후의 get() 호출은 기억한 값을 그대로 반환한다.

val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}

fun main() {
    println(lazyValue)
    println(lazyValue)
}

// computed!
// Hello
// Hello

println(lazyValue)lazy에 전달된 람다를 호출하여 computed가 같이 출력되고 "Hello"를 저장하고, 다음 println(lazyValue)는 저장된 값을 그대로 반환하여 "Hello"만 반환한다.

by viewModels()

val someViewModel: SomeViewModel by viewModels()

프로퍼티 someViewModel은 SomeViewModel 타입이며, Accessor를 viewModels()에 위임한다.

@MainThread
inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
    factory: ViewModelProvider.Factory? = null
): Lazy<VM> = ActivityViewModelLazy(this, VM::class, factory)
/**
 * An implementation of [Lazy] used by [ComponentActivity.viewModels] tied to the given [activity],
 * [viewModelClass], [factory]
 */
class ActivityViewModelLazy<VM : ViewModel>(
    private val activity: ComponentActivity,
    private val viewModelClass: KClass<VM>,
    private val factory: ViewModelProvider.Factory?
) : Lazy<VM> {
    private var cached: VM? = null
    override val value: VM
        get() {
            var viewModel = cached
            if (viewModel == null) {
                val application = activity.application
                        ?: throw IllegalArgumentException("ViewModel can be accessed " +
                                "only when Activity is attached")
                val resolvedFactory = factory ?: AndroidViewModelFactory.getInstance(application)
                viewModel = ViewModelProvider(activity, resolvedFactory).get(viewModelClass.java)
                cached = viewModel
            }
            return viewModel
        }
    override fun isInitialized() = cached != null
}

ActivityViewModelLazy.kt

ActivityViewModelLazyLazy를 구현하여 비슷하게 처음 접근했을 때 ViewModelProvider에서 ViewModel을 가져오며 그 이후에 접근했을 때는 cached에 저장된 ViewModel을 반환한다.

Comments