2023.10.25 - [Language/Kotlin] - [Kotlin] 지연과 위임 - lateinit, by, lazy
[Kotlin] 지연과 위임 - lateinit, by, lazy
lateinit Kotlin은 Java와 다르게 프로퍼티라는 개념이 있고, 주 생성자를 통해 프로퍼티를 선언하며, 프로퍼티는 인스턴스 생성 시점 당시에 초기화가 되어야 한다. Java의 경우에는 Null Safety를 언어
cares-log.tistory.com
코틀린의 위임에 대한 기본 개념은 위 포스팅에 작성되어 있습니다. 참고하시면 좋습니다.
notNull
public fun <T : Any> notNull(): ReadWriteProperty<Any?, T> = NotNullVar()
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
}
public override fun toString(): String =
"NotNullProperty(${if (value != null) "value=$value" else "value not initialized yet"})"
}
`ReadWriteProperty`를 구현한 `NotNullVar` 객체를 리턴한다. getter를 호출할 때 값이 null이라면 `IllegalStateException`을 던진다.
var number: Int by notNull()
`lateinit`과 비슷한 기능이나, `notNull`의 경우에는 위 코드처럼 원시 타입에도 사용가능하다는 점이 다르다. 따라서 `Int`, `Long`과 같은 null이 허용되지 않아야 하는 원시타입 프로퍼티의 초기화 시점을 늦추고 싶은 경우 사용하기 좋다.
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)
}
`ObservableProperty` 객체를 리턴하는 함수이다. 초기 값과, 변경이 되었을 때 실행할 함수를 파라미터로 받고 있다.
public abstract class ObservableProperty<V>(initialValue: V) : ReadWriteProperty<Any?, V> {
private var value = initialValue
protected open fun beforeChange(property: KProperty<*>, oldValue: V, newValue: V): Boolean = true
protected open fun afterChange(property: KProperty<*>, oldValue: V, newValue: V): Unit {}
public override fun getValue(thisRef: Any?, property: KProperty<*>): V {
return value
}
public override fun setValue(thisRef: Any?, property: KProperty<*>, value: V) {
val oldValue = this.value
if (!beforeChange(property, oldValue, value)) {
return
}
this.value = value
afterChange(property, oldValue, value)
}
override fun toString(): String = "ObservableProperty(value=$value)"
}
`ObservableProperty`는 `ReadWriteProperty`를 구현한 추상클래스로, `setValue` 시에 값이 변경되기 전 호출되는 `beforeChange`, `beforeChange` 함수에서 true를 반환할 경우 호출되는 `afterChange`를 `open` 함수로 가지고 있다.
`observable` 함수를 통해 리턴되는 객체는 `afterChange` 함수만 오버라이딩 하고 있기 때문에 `beforeChange` 함수에서는 항상 true만을 만환할 것이고, setter가 호출되는 모든 경우에 `afterChange` 함수가 실행된다.
var level: Int by observable(0) { property, oldValue, newValue ->
println("$oldValue 에서 $newValue 로 단계 상승")
}
기본적으로 위와 같이 값이 변경될 때 로그를 남기는 용도로 사용하기 좋다. `observable`의 함수 파라미터로 넣는 `onChange` 함수는 값이 변경된 이후에 실행되기 때문에 만약 값을 변경하기 전에 무언가 로직이 필요하다면 적합하지 않다. 이럴 경우 `ObservableProperty` 추상클래스를 커스텀하게 구현해야 한다.
var level: Int by object : ObservableProperty<Int>(0) {
override fun beforeChange(property: KProperty<*>, oldValue: Int, newValue: Int): Boolean {
return if((newValue - oldValue).absoluteValue > 10) {
println("한 번에 10단계를 상승할 수는 없습니다.")
false
} else true
}
}
위와 같이 값을 한번에 10 이상 변경하면 안되는 경우일 때 `beforeChange`를 오버라이딩한 `ObservableProperty` 구현체를 만들면 된다.
vetoable
방금 값이 변경되기 전에 무언가 로직이 필요한 경우에 대해 `ObservableProperty` 추상 클래스를 직접 커스텀하게 구현했다. 하지만, 위 예시처럼 사실 `beforeChange`만 구현하는 경우 kotlin이 이미 함수로 만들어 놓았다.
public inline fun <T> vetoable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Boolean):
ReadWriteProperty<Any?, T> =
object : ObservableProperty<T>(initialValue) {
override fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean = onChange(property, oldValue, newValue)
}
따라서 위에 값을 한번에 10 이상 변경하면 안되는 경우에 대한 예제는 사실 아래와 같이 변경하면 더 간단해진다.
var level: Int by vetoable(0) { property, oldValue, newValue ->
if((newValue - oldValue).absoluteValue > 10) {
println("한 번에 10단계를 상승할 수는 없습니다.")
false
} else true
}
만약 `beforeChange`, `afterChange` 둘 다 구현이 필요한 경우는 `ObservableProperty`를 구현하는 방법밖에 없다.
다른 프로퍼티로 위임
@Deprecated("level은 다음 버전부터 지원 중단", ReplaceWith("stage"))
var level: Int = 0
var stage: Int by this::level
`level` 이라는 프로퍼티가 어떤 이유로 다음 버전부터 이용이 불가능하다고 해보자. 이럴 경우 갑자기 `level`을 지우게 되면 오류가 날 수가 있기 때문에 점차적으로 이전 코드를 수정해야 하는 경우 위와 같이 프로퍼티 위임을 활용할 수 있다.
Map에 위임
`by` 뒤에 `Map` 객체를 통해서도 위임할 수 있다. 프로퍼티의 이름에 Key로 해서 `Map`의 Value를 가져온다고 생각하면 된다.
abstract class OAuth2Attribute {
abstract val id: String
abstract val name: String
}
class KakaoAttributes(
attributeMap: Map<String, String>,
): OAuth2Attribute() {
private val identifier: String by attributeMap
override val id: String by this::identifier
override val name: String by attributeMap
}
예를 들어 OAuth2 로그인을 구현하는데, 리소스 제공자마다 스펙이 다르다고 해보자. 이를 좀 공통으로 처리하고 싶어서 `OAuth2Attirbute`라는 공통 추상 클래스를 만들고, 각 제공자마다 구현하도록 설계를 했다고 해보자.
만약 카카오의 경우에 identifier라는 이름으로 계정 식별자를 넘겨준다고 할 때, 위와 같이 `Map`에 위임하여 identifier를 쉽게 가져온 후, `id`는 `identifier` 프로퍼티에 위임하여 설계할 수 있을 것이다.
만약 프로퍼티가 `var` 로 선언된 경우에는 `MutableMap` 으로 위임할 수도 있다. 이 경우, set을 하게 되면 `Map`의 요소가 수정되게 된다.