오늘은 코틀린으로 위임하는 방식에 대해 이야기하고자 한다.
보통 위임 패턴이라고들 하면 여러 가지로 생각이 들텐데 코틀린 && 안드로이드로 특정 짓고 정리해보겠다!
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가지가 있다
Properties
활용Class Delegation
활용by 하면 익숙한 코드가 떠오르지 않는가? 바로 lazy
다
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
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
}
참고로 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를 통해 늦은 초기화를 했는데, 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 활용법에 속한다
kotlin.properties.ReadOnlyProperty
kotlin.properties.ReadWriteProperty
위 두 개를 각각 상속받아 property 활용이 가능하다.
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")
상속 대신 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
는 정말 간편한 위임 키워드다. 이를 통해 여러가지 위임 케이스로 활용해보자!
실제로 우리 서비스에서도 잘 쓰이는 위임 패턴이다. 여러모로 상속보다 가볍고 여러 도메인에 붙여서 재사용할 수 있어서 이점이 많다.
내가 문서에서 정리한 내용은 간략하고 이해쉽게 정리한 것이므로 아래 참조한 링크들에 들어가서 정독해보는 것도 나쁘지 않다 : )
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/