Kotlin

[Kotlin] 프로퍼티 접근자 로직 재활용: 위임 프로퍼티

sh1mj1 2024. 1. 25. 11:46

Kotlin in Action 을 공부하고 Effective kotlin 의 내용을 조금 참조하여 정리한 글입니다.

 

  • 위임 프로퍼티(delegated property)는 코틀린이 제공하는 convention(관례)에 의존하는 독특하면서 강력한 기능이다.
    • 이를 사용하면 값을 backing property 에 단순히 저장하는 것보다 더 복잡한 방식으로 작동하는 프로퍼티를 쉽게 구현할 수 있다.
    • 또 그 과정에서 접근자(getter/setter)로직을 매번 재구현할 필요도 없다.
    • 예를 들어 자신의 값을 필드가 아닌, DB TABLE 이나 브라우저 세션, 맵 등에 저장할 수 있다.
  • 위임은 객체가 직접 작업을 수행하지 않고 다른 도우미 객체가 그 작업을 처리하게 맡기는 디자인 패턴이다.
    • 이 때 도우미 객체를 위임 객체(delegate)라고 부른다.

이전에 인터페이스에 대한 구현을 다른 객체에 위임하는 예시를 보았었다.

이번에는 그 패턴을 프로퍼티에  적용해서 접근자 기능을 도우미 객체가 수행하도록 위임하는 기능을 공부하자.

강력한 기능인만큼 내용이 많으니 천천히 보자.

위임 프로퍼티 소개

위임 프로퍼티의 일반적인 문법

class Foo {
    var p: Type by Delegate()
}

 `p` 프로퍼티는 접근자 로직을 다른 객체(`Delegate` 클래스의 인스턴스)에게 위임한다.

`by` 뒤에 있는 식을 게산해서 위임에 쓰일 객체를 얻는다.

class Foo {
    private val delegate = Delegate() // 컴파일러가 생성한 도우미 프로퍼티
    // p 프로퍼티를 위해 컴파일러가 생성한 접근자는 delegate 의 getValue 와 setValue 메서드를 호출한다
    var p : Type
        set(value: Type) = delegate.setValue(...) , value)
        get() = delegate.getValue(...)
}

 위처럼 컴파일러는 숨겨진 도우미 프로퍼티(hidden helper property, 위에서는 `delegate`)를 만들고 그 프로퍼티를 위임 객체의 인스턴스로 초기화한다.

`p` 프로퍼티는 바로 그 위임 객체(`delegate`)에게 자신의 작업을 위임한다.

  • 프로퍼티 위임 관례를 따르는 `Delegate` 클래스는 `getValue`(와 `setValue`) 메서드를 제공해야 한다.
  • `getValue` 와 `setValue` 는 멤버 메서드일 수도, 확장 함수일 수도 있다.(일단은 두 메서드의 파라미터 생략, 아래에서 설명)

`Delegate` 클래스를 단순화해서 보기

class Delegate {
    operator fun getValue(...) { ... } // 게터를 구현하는 로직을 담는다.
    operator fun setValue(..., value: Type) { ... } // 세터를 구현하는 로직을 담는다.
}

class Foo {
    var p: Type by Delegate() // by 키워드는 프로퍼티 p 와 위임 객체 delegate 를 연결함.
} 

val foo = Foo()
val oldValue = foo.p // foo.p 라는 프로퍼티 호출은 내부에서 delegate.getValue(...) 를 호출함.
foo.p = newValue     // 프로퍼티 값을 변경하는 문장은 내부에서 delegate.setValue(..., newValue) 를 호출.

 `foo.p` 는 일반 프로퍼티처럼 쓸 수 있으며, 일반 프로퍼티처럼 보인다.

하지만 실제로는 `p` 의 getter/setter 는 `Delegate` 타입의 위임 프로퍼티 객체에 있는 메서드를 호출한다.

위임 프로퍼티 사용: by lazy() 를 사용한 프로퍼티 초기화 지연

코틀린 lib 는 프로퍼티 위임을 사용해 프로퍼티 초기화를 지연시켜줄 수 있다. 

  • 지연 초기화(lazy initialization)은 객체의 일부분을 초기화하지 않고 남겨두었다가 실제로 그 부분의 값이 필요할 경우 초기화할 때 흔히 쓰이는 패턴이다. 
    아래 상황에서 지연 초기화 패턴을 사용할 수 있다.
    • 초기화 과정에서 자원을 많이 사용할 때
    • 객체를 사용할 때마다 꼭 초기화하지 않아도 되는 프로퍼티

예를 들어 `Person` 클래스가 자신이 작성한 이메일의 목록을 제공한다고 하자.

이메일은 DB 에 들어있어서 불러오는데 오래 걸린다. 

그래서 이메일 프로퍼티 값을 최초로 사용할 때 단 한 번만 이메일을 DB 에서 가져온다고 하자.

 

지연 초기화를 backing property 를 통해 구현하기

class Email { /* ... */ }

// DB 에서 이메일을 가져오는 함수
fun loadEmails(person: Person): List<Email> {
    println("${person.name} 의 이메일 가져옴")
    return listOf(/* ... */)
}

class Person(val name: String) {
    private var _emails: List<Email>? = null // 데이터를 저장하고 emails 의 위임 객체 역할을 하는 _emails 프로퍼티 (backing property)
    val emails: List<Email>
        get() {
            if (_emails == null) {
                _emails = loadEmails(this) // 최초 접근 시 이메일을 가져옴.
            }
            return emails // 저장해둔 데이터가 있으면 그 데이터를 반환.
        }
}

val p = Person("AA")
p.emails // 최초로 emails 를 읽을 때 단 한번만 이메일을 가져옴.
Load emails for AA

p.emails

 여기서는 backing property(뒷받침하는 프로퍼티)라는 기법을 사용한다.

`_emails` 라는 프로퍼티는 값을 저장하고, 다른 프로퍼티인 `emails` 는 `_emails` 라는 프로퍼티에 대한 읽기 연산을 제공한다.

`_emails` 는 nullable 인 반면, emails 는 not-null 타입이므로 프로퍼티를 두개 사용해야 한다.

 

하지만 이런 코드를 만드는 것은 성가시다.

지연 초기화해야 하는 프로퍼티가 많아지면 너무 복잡해지며, 이 구현은 thread-safe 하지 않아서 항상 제대로 작동하지는 않는다.

 

위임 프로퍼티를 사용하면 이 코드가 훨씬 더 간단해진다.

위임 프로퍼티는 데이터를 저장할 때 쓰이는 backing property 와 값이 오직 한 번만 초기화됨을 보장하는 getter 로직을 함께 캡슐화해준다.

위임 객체를 리턴하는 라이브러리 함수가 바로 `lazy` 이다. (코틀린 공식 문서)

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

 

지연 초기화를 위임 프로퍼티를 통해 구현하기

class Person(val name: String) {
    val emails by lazy { loadEmails(this) }
}
  • ` lazy` 함수는 코틀린 관례에 맞는 시그니처의 `getValue` 메서드가 들어있는 객체를 리턴한다.
  • 따라서 `lazy` 를 `by` 키워드와 함께 사용하여 위임 프로퍼티를 만들 수 있다.
    • `lazy` 함수의 인자는 값을 초기화할 때 호출할 람다이다.
    • `lazy` 함수는 기본적으로 thread-safe 하다.
      • 필요에 따라 동기화에 사용할 lock(락) 을 `lazy` 함수에 전달할 수도 있다.
      • 멀티 스레드 환경에서 사용하지 않을 프로퍼티를 위해 `lazy` 함수가 동기화를 하지 못하게 막을 수도 있다.

위임 프로퍼티 구현

만약 어떤 객체의 프로퍼티가 바뀔 때마다 리스너에게 변경 통지를 보내고 싶다고 하자.

`observable` 패턴을 구현하면 된다. 

  • 예: 어떤 객체를 UI 에 표시하는 경우 객체가 바뀌면 자동으로 UI 도 바뀌어야 한다. 

위임 프로퍼티 없이 기능 구현

자바에서는 `PropertyChangeSupport` 와 `PropertyChangeEvent` 클래스를 사용해 이런 통지를 자주 처리한다.

  • `PropertyChangeSupport` 클래스는 
    • 리스너의 목록을 관리하고 `PropertyChangeEvent` 이벤트가 들어오면 목록의 모든 리스너에게 이벤트를 통지한다.
    • 자바 빈 클래스의 필드에 `PropertyChangeSupport` 인스턴스를 저장하고 프로퍼티 변경 시 그 인스턴스에게 처리를 위임한다.

`PropertyChangeAware`: `PropertyChangeSupport` 를 사용하기 위한 도우미 클래스 

open class PropertyChangeAware {
    protected val changeSupport = PropertyChangeSupport(this)
    fun addPropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.addPropertyChangeListener(listener)
    }

    fun removePropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.removePropertyChangeListener(listener)
    }
}

 `PropertyChangeSupport` 인스턴스를 `changeSupport` 필드에 저장하고 프로퍼티 변경 리스너를 추적해주는 도우미 클래스를 만들었다.

리스너 지원이 필요한 클래스는 이 도우미 클래스를 확장해서 `changeSupport` 에 접근할 수 있다.

 

`Person`: 프로퍼티 변경 통지를 직접 구현하기

class Person(
    val name: String, age: Int, salary: Int
) : PropertyChangeAware() {
    var age: Int = age
        set(newValue) {
            val oldValue = field
            field = newValue
            changeSupport.firePropertyChange("age", oldValue, newValue) // 프로퍼티 변경을 리스너에게 통지
        }
    var salary: Int = salary
        set(newValue) {
            val oldValue = field
            field = newValue
            changeSupport.firePropertyChange("salary", oldValue, newValue)
        }
}

val p = Person("Dmitry", 34, 2000)
p.addPropertyChangeListener( // 프로퍼티 변경 리스너를 추가
    PropertyChangeListener { event ->
        println(
            "Property ${event.propertyName} changed" +
                    "from ${event.oldValue} to ${event.newValue}"
        )
    }
)
p.age = 35 // print /* Property age changed from 34 to 35 */
p.salary = 2100 // print /* Property salary changed from 2000 to 2100

 `field` 키워드를 통해서 `age` 와 `salary` 프로퍼티를 backing field 에 접근하고 있다. (backing field 에 접근하기)

setter 코드를 보면 중복이 많이 보인다.

`ProperyChangeSupport`
이 클래스는 `listener` 목록을 관리하고 `PropertyChangeEvent` 를 `listener` 에게 발송한다.
이 클래스의 인스턴스를 bean 의 멤버 필드로 사용하고 이러한 작업들을 해당 클래스에 위임할 수 있다.

`PropertyChangeListener`
bean 이 bound 프로퍼티를 변경할 때마다 `PropertyChange` 이벤트가 발생한다.
바인딩된 프로퍼티 업데이트에 대한 알림을 받을 수 있도록 source bean 에 `PropertyChangeListener` 를 등록할 수 있다.

`PropertyChangeEvent`
Bean이 "bound" 또는 "constraint" 속성을 변경할 때마다 "PropertyChange" 이벤트가 전달된다.
`PropertyChangeEvent` 개체는 `PropertyChangeListener` 및 `VoitableChangeListener` 메서드에 인수로 전송된다.
...

프로퍼티 값 저장, 변경 통지하는 클래스 추출

프로퍼티 값을 저장하고 필요에 따라 통지(nofitication)을 보내주는 클래스를 추출해보자

 

`ObservableProperty` 로 리팩토링: 도우미 클래스를 통해 프로퍼티 변경 통지(notify) 구현하기

class ObservableProperty(
    val propName: String, var propValue: Int, val changeSupport: PropertyChangeSupport
) {
    fun getValue(): Int = propValue
    fun setValue(newValue: Int) {
        val oldValue = propValue
        propValue = newValue
        changeSupport.firePropertyChange(propName, oldValue, newValue)
    }
}

class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
    val _age = ObservableProperty("age", age, changeSupport)
    var age: Int
        get() = _age.getValue()
        set(value) { _age.setValue(value) }
    val _salary = ObservableProperty("salary", salary, changeSupport)
    var salary: Int
        get() = _salary.getValue()
        set(value) { _salary.setValue(value) }
}

 이 코드는 코틀린의 위임이 실제로 작동하는 방식과 비슷하다. 

setter 에서의 동작이 `ObservableProperty` 클래스로 위임되어 중복되었던 setter 로직이 많이 간소화되었다.

프로퍼티 값을 저장하고, 그 값이 바뀌면 자동으로 변경 통지를 전달해주는 클래스(`ObservableProperty`)를 만들었고, 로직의 중복을 상당 부분 제거했다. 

 

하지만 아직도 각각의 프로퍼티마다 `ObservableProperty` 를 만들고 getter/setter 에서 `ObservableProperty` 에 작업을 위임하는 준비 코드가 상당 부분 필요하다. 

위임 프로퍼티 사용하여 리팩토링: boilerplate 코드 없애기

코틀린 위임 프로퍼티 기능을 활용하면 이런 준비 코드를 없앨 수 있다.

위임 프로퍼티를 쓰기 전에 `ObservableProperty` 에 있는 두 메서드의 시그니처를 코틀린의 관례에 맞게 수정해야 한다.

 

`ObservableProperty` 를 프로퍼티 위임에 사용할 수 있도록 변경

class ObservableProperty(
    var propValue: Int, val changeSupport: PropertyChangeSupport
) {
    operator fun getValue(p: Person, prop: KProperty<*>): Int = propValue
    operator fun setValue(p: Person, prop: KProperty<*>, newValue: Int) {
        val oldValue = propValue
        propValue = newValue
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }
}
  • ` getValue`/`setValue` 함수에도 operator 변경자가 붙음.
  • `getValue` 와 `setValue` 는 프로퍼티가 포함된 객체(`p: Person`)프로퍼티를 표현하는 객체(`prop: KProperty<*>`)를 파라미터로 받는다.
    `KProperty` 내부에 대해서는 코틀린 리플렉션으로 따로 다루겠다.
    지금은 `KProperty.name` 을 통해 메서드가 처리할 프로퍼티 이름을 알 수 있다는 점만 기억하자.
  • `KProperty` 인자를 통해 프로퍼티 이름을 전달받으므로 주 생성자에서는 `proName` 프로퍼티를 없앤다.

이제 위임 프로퍼티를 통해서 프로퍼티 변경 통지 받아보자

class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
    var age: Int by ObservableProperty(age, changeSupport)
    var salary: Int by ObservableProperty(salary, changeSupport)
}

 `by` 키워드를 통해 위임 객체를 지정하면 이전에 코드를 직접 짜야 했던 여러 작업을 코틀린 컴파일러가 자동으로 처리해준다.

코틀린 컴파일러가 만들어주는 코드는 가장 처음 버전의 `Person` 과 비슷하다.

 

`by` 오른쪽에 오는 객체를 위임 객체(delegate)라고 부른다.

코틀린은 위임 객체를 감춰진(hidden) 프로퍼티에 저장하고, 주 객체의 프로퍼티를 읽거나 쓸 때 위임 객체의 getValue 와 setValue 를 호출해준다.

코틀린의 Delegates.observable 을 사용하는 것으로 리팩토링

kotlin stlib 에는 이미 `ObservableProperty` 와 비슷한 기능 `Delegates.observable` 가 있다.

이 클래스는 `PropertyChangeSupport` 와는 연결되어 있지 않다. 

따라서 프로퍼티 값의 변경을 통지할 때 `PropertyChangeSupport` 를 사용하는 방법을 알려주는 람다를 넘겨야 한다.

 

`Delegates.observable` 을 사용해 프로퍼티 변경 통지 구현하기

class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
    private val observer = { prop: KProperty<*>, oldValue: Int, newValue: Int ->
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }
    var age: Int by Delegates.observable(age, observer)
    var salary: Int by Delegates.observable(salary, observer)
}

 `by` 의 오른쪽에 있는 식이 꼭 새 인스턴스를 만들 필요는 없다.

함수 호출, 다른 프로퍼티, 다른 식 등이 `by` 의 우항에 올 수 있다.

다만 우항에 있는 식을 계산한 결과인 객체는 컴파일러가 호출할 수 있는 올바른 타입의 `getValue`/`setValue` 를 반드시 제공해야 한다. 

`getValue`/`setValue` 모두 멤버 메서드이거나 확장 함수일 수 있다.

 

간단하게 `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")
}

 여기서 `listOf` 와 `null` 은 초기값이다.

`Delegates.observable` 함수
public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
        ReadWriteProperty<Any?, T> =
    object : ObservableProperty<T>(initialValue) {
        override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
    }​

이 함수는 프로퍼티에 대한 읽기/쓰기 프로퍼티 위임자(read/write property delegate)를 생성하는 역할을 한다.
생성된 프로퍼티 대리자는 속성의 값이 변경될 때 지정된 콜백 함수를 호출한다.

함수의 리턴값은 `ReadWriteProperty<Any?, T>` 타입의 `ObservableProperty` 객체인 프로퍼티 위임자이다.
이 프로퍼티 위임자를 사용하면 속성 값이 변경될 때마다 argument 로 넘긴 `onChange` 콜백 함수가 호출되어 추가 동작을 수행할 수 있다.

코드 내의 `ObservableProperty` 클래스는 변경 후에 `afterChange` 메서드를 호출하여 실제로 콜백 함수를 실행한다.
이 콜백 함수에는 속성의 변경 전후 값 및 속성에 대한 정보가 전달된다.

위임 프로퍼티 컴파일 규칙

아래와 같이 위임 프로퍼티가 있는 클래스가 있다고 하자.

class C {
    var prop: Type by MyDelegate()
}

val c = C()

 컴파일러는 `MyDelegate` 클래스의 인스턴스를 감춰진 프로퍼티(Hidden property)에 저장하고 그 감춰진 프로퍼티를 `<delegate> ` 라는 이름으로 부른다.

 

또한 컴파일러는 프로퍼티 레퍼런스를 위해 `KProperty` 타입의 객체를 사용한다. 

이 `KProperty` 는 한 클래스에 여러 프로퍼티가 있을 수 있으므로 프로퍼티를 식별하기 위해 필요하다.

이 객체를 `<property>` 라고 부른다.

 

컴파일러는 아래와 바숫한 코드를 생성한다.

class C {
    private val <delegate> = MyDelegate()
    var prop: Type
        get() = <delegate>.getValue(this, <property>)
        set(value: Type) = <delegate>.setValue(this, <property>, value)
}

컴파일러는 프로퍼티의 접근자 안에 `getValue`/`setValue` 호출 코드를 만들어준다.

프로퍼티를 사용하면 <delegate> 에 있는 getValue 나 setValue 함수가 호출된다.

Effective kotlin 에서는 아래와 같은 예시 코드를 보여준다.

class LoggingProperty<T>(var value: T) {
    operator fun getValue(thisRef: Any?, prop: KProperty<*>): T {
        println("${prop.name} returned value $value")
        return value
    }

    operator fun setValue(thisRef: Any?, prop: KProperty<*>, newValue: T) {
        val name = prop.name
        println("$name changed from $value to $newValue")
        value = newValue
    }
}

class PropertyDelegate {
    var token: String? by LoggingProperty(null)
    var attempt: Int by LoggingProperty(0)
}

// ↓↓↓ 아래처럼 컴파일됨 ↓↓↓

@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)
    }

 Effective kotlin 에서는 `<delegate>` 를 `token$delegate` 로, `<property>` 를 `::token` 으로 표현하고 있다. (예시에서 프로퍼티 이름이 `token` 임)

 

여기서 this 는 컨텍스트를 나타낸다.

컨텍스트 함수는 어떤 위치에서 사용되는지와 관련된 정보를 제공해준다.

getValue 와 setValue 를 여러 개 갖기

getValue 와 setValue 메서드를 여러 개 있어도 컨텍스트(위 코드에서 `this`)를 활용하므로 상황에 따라 적절한 메서드가 선택된다.

예를 들어 여러 종류와 뷰와 함께 사용할 수 있는 Delegate 가 필요할 수 있다.

 

`Activity` 와 `Fragment` 에 함께 사용할 수 있는 Delegate 가 필요한 예

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 }
    }
}

 

이 매커니즘은 단순하지만 활용법이 많다.

  • 프로퍼티 값이 저장될 장소를 바꿀 수 있다.
    • 맵, DB table, 사용자 세션의 쿠키 등
  • 프로퍼티를 읽거나 쓸 때 벌어질 일을 커스텀할 수 있다.
    • 값 검증, 변경 통지 등

이 모든 동작들이 매우 간결한 코드로 동작하게 된다.

프로퍼티 값을 맵에 저장

  • 자신의 프로퍼티를 동적으로 정의할 수 있는 객체를 만들 때 위임 프로퍼티를 자주 활용한다.
    • 그런 객체를 확장 가능한 객체(expando object) 라고 부른다.

값을 맵에 저장하는 프로퍼티 정의하기

class Person {
    // 추가 정보
    private val _attributes = hashMapOf<String, String>()
    fun setAttribute(attrName: String, value: String) {
        _attributes[attrName] = value
    }

    // 필수 정보
    val name: String // 수동으로 맵에서 정보를 꺼낸다
        get() = _attributes["name"] ?: throw IllegalStateException("name is not initialized") 
}

val p = Person()
val data = mapOf("name" to "Dmitry", "company" to "JetBrains")
for ((attrName, value) in data) {
    p.setAttribute(attrName, value)
}
assert(p.name == "Dmitry")

 이 코드에서는 추가 데이터를 일반 API 를 사용하여 저장하고, 특정 프로퍼티(`name`)을 읽을 때 특별한 개별 API 를 사용한다.

위 코드도 위임 프로퍼티를 활용하여 쉽게 변경할 수 있다.

 

값을 맵에 저장하는 위임 프로퍼티 사용하기

class Person {
    private val _attributes = hashMapOf<String, String>()
    fun setAttribute(attrName: String, value: String) {
        _attributes[attrName] = value
    }
    
    val name: String by _attributes // 위임 프로퍼티로 map 을 사용
}

 이렇게 `by` 키워드 뒤에 맵을 직접 넣으면 된다.

 

이 코드가 작동하는 이유는  stlib 가 `Map` 과 `MutableMap` 인터페이스에 대해 `getValue` 와 `setValue` 확장 함수를 제공하기 때문이다.

위 코드에서 `by` 를 타고 들어가면 아래와 같은 코드를 볼 수 있다.

@file:kotlin.jvm.JvmName("MapAccessorsKt")

package kotlin.collections
// ...

@kotlin.internal.InlineOnly
public inline operator fun <V, V1 : V> Map<in String, @Exact V>.getValue(thisRef: Any?, property: KProperty<*>): V1 =
    @Suppress("UNCHECKED_CAST") (getOrImplicitDefault(property.name) as V1)

@kotlin.jvm.JvmName("getVar")
@kotlin.internal.InlineOnly
public inline operator fun <V, V1 : V> MutableMap<in String, out @Exact V>.getValue(thisRef: Any?, property: KProperty<*>): V1 =
    @Suppress("UNCHECKED_CAST") (getOrImplicitDefault(property.name) as V1)

@kotlin.internal.InlineOnly
public inline operator fun <V> MutableMap<in String, in V>.setValue(thisRef: Any?, property: KProperty<*>, value: V) {
    this.put(property.name, value)
}

 `getValue` 에서 맵에 프로퍼티 값을 저장할 때는 자동으로 프로퍼티 이름을 키로 활용한다.

이렇게 `getValue` 와 `setValue` 를 위처럼 확장함수로도 만들 수 있다는 것을 보았다.

다양한 위임 매커니즘 활용 예시

위에서 살펴본 지연 프로퍼티와 observable 패턴 외의 다양한 위임 매커니즘 활용 예시를 보자.

  • 안드로이드에서의 뷰,리소스 바인딩의 예
private val button: Button by bindView(R.id.button)
private val textXize by bindDimension(R.dimen.font_size)
private val doctor: Doctor by argExtra(DOCTOR_ARG)
  • Koin(DI 라이브러리)에서의 의존성 주입 예
private val presenter: MainPresenter by inject()
private val repository: NetworkRepository by inject()
private val vm: MainViewModel by viewModel()
  • 데이터 바인딩의 예
private val port by bindConfiguration("port")
private val token: String by preferences.bind(TOKEN_KEY)

프레임 워크에서 위임 프로퍼티 활용

객체 프로퍼티를 저장/변경 하는 방법을 바꿀 수있으면 프레임워크를 개발할 때 유용하다.

Exposed 프레임워크 는 SQL DB 의 구조를 기술할 할 수 있는 DSL 을 제공한다.

이 프레임워크를 사용하는 예를 들어보자.

DSL 의 관련된 내용은 다룰 게 많으므로 이 때 위임 프로퍼티를 쓸 수 있다는 부분만을 집중해서 보자.

 

DB 에 `User` 테이블이 있고 그 테이블에는 `name` 문자열 타입 칼럼(column) 과 `age` 정수 타입 column 이 있다고 하자. 

`Users` 와 `User` 라는 클래스를 코틀린에서 정의할 수 있다. 

DB 에 들어있는 모든 `User` 엔티티를 `User` 클래스를 통해 가져오고 저장할 수 있다. 

 

위임 프로퍼티를 사용해 DB 칼럼에 접근하기

object Users : IdTable() { // 이 객체는 DB table 이다. 
    // 아래 프로퍼티들은 테이블의 칼럼이다.
    val name = varchar("name", length = 50).index() 
    val age = integer("age")
}

// 각 User 인스턴스는 테이블에 들어있는 구체적인 엔티티에 해당한다.
class User(id: EntityID) : Entity(id) { // 사용자 이름은 DB 의 "name" 칼럼에 들어있다.
    var name: String by Users.name
    var age: Int by Users.age
}
  • `Users` 객체: DB 테이블.  DB 전체에 단 하나만 존재하므로 싱글턴 객체이다.
  • `Users` 객체의 프로퍼티: DB 테이블의 칼럼

`User` 의 상위 클래스인 Entity 클래스는 DB 칼럼을 엔티티의 속성(attribute)값으로 연결해주는 매핑이 있다.

각 `User` 의 프로퍼티 중에는 DB 에서 가져온 `name` 과 `age` 가 있다. 

 

이 프레임워크를 사용하면 `User` 의 프로퍼티에 접근할 때 자동으로 `Entity` 클래스에 정의된 DB 매핑으로부터 필요한 값을 가져오기 때문에 편리하다.

어떤 `User` 객체를 변경하면 그 객체는 변경됨(dirty) 상태로 변하고, 프레임워크는 나중에 적절히 DB 에 변경 내용을 반영한다. 

 

각 엔티티 속성(`User` 인스턴스의 `name`, `age`)은 칼럼 객체(`Users.name`, `Users.age`)를 위임 객체로 사용하는 위임 프로퍼티이다. 

프레임워크는 `Column` 클래스 안에 `getValue` 와 `setValue` 메서드를 정의한다. 

두 메서드는 DB 의 데이터를 읽고/쓰는 작업을 하며, 코틀린의 위임 객체 관례에 따른 시그니처 요구사항을 만족한다. 

아래 모습과 비슷하게 되어 있을 것이다.

operator fun <T> Column<T>.getValue(o: Entity, desc: KProperty<*>: T) {
    // DB 에서 칼럽 값 가져오기
}

operator fun <T> Column<T>.setValue(o: Entity, desc: KProperty<*>, value:T) {
    // DB 의 값 변경
}

 `Column` 프로퍼티(`Users.name`)을 위임 프로퍼티(`User.name`)에 대한 위임 객체로 사용할 수 있다. 

`user.age += 1` 은 `user.ageDelegate.setValue(user.ageDelegate.getValue() + 1)` 과 비슷한 코드로 변환된다.

나중에 Exposed 프레임워크에서 사용한 DSL 설계 기법에 대해 자세히 다룰 것이다.

일반적인 프로퍼티 패턴은 프로퍼티 위임으로 만들어라.

Effective kotlin item 21 에서 위처럼 말하고 있다.

프로퍼티 위임(property delegation)을 사용하면 일반적인 프로퍼티의 행위를 추출해서 재사용할 수 있다. 

지금까지의 코드는 모두 일반적인 프로퍼티 패턴을 프로퍼티 위임으로 만든 과정들이다.

 

코틀린 stlib 의 대표적은 프로퍼티 `Delegator`

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

위는 굉장히 범용적으로 사용되는 패턴들에 대한 프로퍼티 델리게이터이다.

직접 프로퍼티 델리게이터를 만들어서 사용할 수도 있다.