Android 코틀린 위임 패턴

woga·2023년 4월 4일
1

Android 공부

목록 보기
43/49

오늘은 코틀린으로 위임하는 방식에 대해 이야기하고자 한다.
보통 위임 패턴이라고들 하면 여러 가지로 생각이 들텐데 코틀린 && 안드로이드로 특정 짓고 정리해보겠다!

위임 패턴? (Delegation pattern)

In software engineering, the delegation pattern is an object-oriented design pattern that allows object composition to achieve the same code reuse as inheritance.

위키피디아에서 위임 패턴은 상속과 비슷하게 코드 재사용을 객체로 구성하는 것이라고 말한다 즉!

상속이란 방법 대신 위임이란 방식으로 상속만큼 코드의 재사용성을 강력하게 만드는 것이다.

대략 UML 다이아그램으로 확인해보면 아래와 같다

그리고 이들은 아래처럼 구성된다.

  • A delegating object to keep the reference of the delegate. (delegate 할 객체를 참조할 객체가 있고)

  • An interface to declare how the methods will be implemented inside the delegate. (어떻게 안에서 delegate할 메서드를 interface로 정의하고)

  • A helper object that will implement the methods defined inside the delegate interface. (delegate 인터페이스의 구현체인 객체가 존재한다)

다이아그램처럼 ISoundBehavior 인터페이스는 원하는 동작인 소리를 낸다를 정의해서 핵심 구조를 유지했다. 그리고 이를 상속받은 구현체 MeowSound는 메서드에 대해 구현해놓는다. 그리고 Cat 클래스는 MeowSound 클래스의 인스턴스를 생성하고 요청을 위임한다.

그럼 대충 어떤 방식으로 위임하는지 감이 오는가? 더 자세하게 코틀린에서의 위임 패턴들의 방식을 살펴보자

코틀린에서의 위임 패턴?

주로 어떤게 있을까 생각하면 2가지가 있다

  • by 키워드를 활용한 Properties 활용
  • interface를 활용한 Class Delegation 활용

by 키워드를 활용한 Properties 활용

by 하면 익숙한 코드가 떠오르지 않는가? 바로 lazy

  • lazy의 기본 코드
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
  • lazy 활용 코드
private val viewModel: MainViewModel by lazy {
	MainViewModel()
}

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

기본 코드에서 눈에 띄는 Lazy<T>는 최상위 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
}

참고로 SynchronizedLazyImplby 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 viewModels를 끼얹으면?

by 키워드가 붙어서 눈치챘겠지만 이 친구도 위임 패턴을 사용했다.

앞에서는 lazy를 통해 늦은 초기화를 했는데, viewModels 역시 늦은 초기화는 내부에 lazy를 활용한다

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

createViewModelLazy 함수를 호출하는 모습이 보이는가

@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<>를 활용하고 있다.

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

Delegated Properties

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

  • kotlin.properties.ReadOnlyProperty

  • kotlin.properties.ReadWriteProperty

위 두 개를 각각 상속받아 property 활용이 가능하다.

  • ReadWritePropery example
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
    }
}

외부에서 값을 주입하고 변경이 가능하다

만약 특정 값을 체크하거나 안전하게 사용할거면 아래처럼 활용할 수 있다

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")

interface로 Class Delegation 활용

상속 대신 delegation을 활용할 수 있다. 이 역시 by 키워드를 활용해서 처리하는데 예제를 살펴보자

interface Wish {
	fun addWish()
}

class WishImpl(): Wish {
	ovverid fun addWish() { 
    	println("add the product!")
    }
}

class Product(wish: Wish) : Wish by wish

fun main() {
	val wish = WishImpl()
    Product(wish).addWish()
}

물론 아래와 같이 바로 초기화해서 사용도 가능하다

interface Wish {
	fun addWish()
}

class WishImpl(): Wish {
	ovverid fun addWish(val id: Int) { 
    	println("$id add the product!")
    }
}

class Product() : Wish by WishImpl()

fun main() {
	val id = getProductId()
    Product().addWish(id)
}

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

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

그래서 Product 클래스에서 바로 addWish() 함수 호출이 가능하다


class Product : Wish by WishImpl() {
	fun clickWish() {
    	addWish()
    }
}

마무리

코틀린에서 by는 정말 간편한 위임 키워드다. 이를 통해 여러가지 위임 케이스로 활용해보자!

실제로 우리 서비스에서도 잘 쓰이는 위임 패턴이다. 여러모로 상속보다 가볍고 여러 도메인에 붙여서 재사용할 수 있어서 이점이 많다.

내가 문서에서 정리한 내용은 간략하고 이해쉽게 정리한 것이므로 아래 참조한 링크들에 들어가서 정독해보는 것도 나쁘지 않다 : )

Reference

https://en.m.wikipedia.org/wiki/Delegation_pattern
https://proandroiddev.com/delegation-in-kotlin-e1efb849641
https://prokash-sarkar.medium.com/delegation-pattern-an-effective-way-of-replacing-androids-baseactivity-with-native-kotlin-support-b00dee007d69
https://thdev.tech/kotlin/2020/11/27/kotlin_delegation/

profile
와니와니와니와니 당근당근

0개의 댓글