본문 바로가기

개발/Kotlin

Item 21: Use property delegation to extract common property patterns

 코드 재사용을 지원하도록 코틀린이 소개한 한 가지 새로운 기능은 Property delegation이다.

이를 사용한 두 가지 예시를 살펴보자.

 

 첫 번째로 코틀린에서는 stdlib 내부에 lazy라는 함수가 있다. 이것은 lazy property pattern을 구현한 property delegate를 return 한다.

 

val value by lazy { createValue() }

 

 이렇게 사용하면 value라는 property가 사용되는 시점에 createValue가 호출된 뒤에 초기화 된다.

 두 번째는 observer 패턴이다. 안에 있는 데이터가 바뀌면 그에 맞춰서 item들을 다시 그려야 하는 상황이 있거나 모든 변화에 대한 로그를 남겨야한다고 생각해보면 stdlib의 observable 패턴을 구현해서 해결할 수 있다.

 

var items: List<Item> by
	Delegates.observable(listOf()) { _, _, _ ->
    	notifyDataSetChanged()
    }
    
var key: String? by
    Delegates.observable(null) { _, old, new ->
    	Log.e("key changed from $old to $new")
    }

 

 

Java에서 View, Resource Binding, Dependency Injection 혹은 Data Binding을 하려면 annotation processing과 같은 작업이 필요하지만, kotlin에서는 property delegation을 통해 쉽고 type-safe하게 구현하도록 할 수 있다.

 

 

// View and resource binding example in Android
private val button: Button by bindView(R.id.button)
private val textSize: by bindDimension(R.dimen.font_size)

// Dependency Injection using Kotlin
private val presenter: MainPresenter by inject()
private val vm: MainViewModel by viewModel()

// Data binding
private val port by bindConfiguration("port")
private val token: String by preferences.bind(TOKEN_KEY)

 

 이게 어떻게 동작하는 지 알아보기 위해서 쉬운 것 부터 살펴보자.

우리가 property들이 어떤 식으로 사용되는 지 추적하기 위해 로그를 찍는 custom getter, setter를 추가했다고 가정하자.

 

var token: String? = null
    get() {
    	print("token returened value $field")
        return field
    }
    set(value) {
    	print("token changed from $field to $value")
        field = value
    }
    
var attemps: Int = 0
    get() {
    	print("attemps returened value $field")
        return field
    }
    set(value) {
    	print("attemps changed from $field to $value")
        field = value
    }

 

 두 property의 타입은 다르지만 행위는 거의 동일하다고 볼 수 있다.

 이 비슷한 두 가지의 행위는 property delegation으로 추출되서 사용할 수 있다. Delegation은 property가 그 val의 getter나 var의 getter, setter와 같은 accessor에 의해 정의된다는 아이디어에서 비롯되었다.

 이러한 메소드들은 다른 오브젝트의 메소드로 위임될 수 있다.

 

var token: String? by LoggingProperty(null)
var attempts: Int by LoggingProperty(0)

private class LoggingProperty<T>(var value:T) {
    operator fun getValue(
    	thisRef: Any?,
        prop: KProperty<*>
    ): T {
    	print("${prop.name} returned value $value")
        return value
    }
    
    operator fun setvalue(
    	thisRef: Any?,
        prop: KProperty<*>,
        newValue:T
    ) {
        val name = prop.name
        print("$name changed from $value to $newValue")
        value = newValue
    }
 }

 

K-Property 펄~럭

 

KProperty - Kotlin Programming Language

 

kotlinlang.org

 Property delegation이 작동하는 원리를 완벽히 알기 위해 "by"가 어떻게 컴파일 되는지 살펴보자.

 

@JvmField
private val `token$delegate` =
	LoggingProperty<String?>(null)
var token: String?
    get() = `token$delegate`.getValue(this, ::token)
    set(value) {
    	`token$delegate`.setValue(this, ::token, value)
    }

 

 위와 같이 getValue와 setValue는 value에만 작동하는게 아니라 property, context(this)에 대한 제한적 참조도 가능하다.

여러 개의 getValue와 setValue 메소드가 있으면 context에 의해 적절한 메소드를 찾아준다.

 

class SwipeRefreshBinderDelegate(val id: Int) {
	private var cache: SwipeRefreshLayout? = null
    
    operator fun getValue(
    	activity: Activity,
        prop: Kproperty<*>
	): SwipeRefreshLayout {
    	return cache ?: activity
        	.findViewById<SwipeRefreshLayout>(id)
            .also { cache = it }
    }
    
    operator fun getValue(
        fragment: Fragment,
        prop: Kproperty<*>
	): SwipeRefreshLayout {
    	return cache ?: fragment.view
        	.findViewById<SwipeRefreshLayout>(id)
            .also { cache = it }
    }
}

 

 Object를 property delegate로 사용하기 위해서는 필요한 것은 val는 getValue, var은 getValue, setValue 연산자이다. 이런 연산자는 멤버 함수가 될 수 있지만 kotlin stdlib에 있는 extension 일 수도 있다.

 

val map: Map<String, Any> = mapOf(
	"name" to "Marcin",
   	"kotlinProgrammer" to true
)
val name by map
print(name) // Marcin
inline operator fun <V, V1: V> Map<in String, V>
.getValue(thisRef: Any?, property: KProperty<*>): V1=
getOrImplicitDefault(property.name) as V1

 

 Kotlin standard library에는 반드시 알아야 할 몇 가지 property delegate가 있다.

  • lazy
  • Delegates.observable
  • Delegates.vetoable
  • Delegates.notNull