(Kotlin/코틀린) ObservableProperty / VetoableProperty 완전 정리


1. Delegates.observable

값이 변경될 때마다 변경 후 콜백을 받습니다.

import kotlin.properties.Delegates

var name: String by Delegates.observable("초기값") { property, oldValue, newValue ->
    println("${property.name}: '$oldValue' → '$newValue'")
}

name = "Alice"  // name: '초기값' → 'Alice'
name = "Bob"    // name: 'Alice' → 'Bob'
// 람다 파라미터
Delegates.observable(initialValue) { property, old, new ->
    // property: KProperty<*> — 프로퍼티 메타데이터 (이름 등)
    // old: 이전 값
    // new: 새 값
    // 반환값 없음 (Unit)
}

2. Delegates.vetoable

변경 콜백이 호출되며, false를 반환하면 변경이 거부됩니다.

var positiveNumber: Int by Delegates.vetoable(0) { property, oldValue, newValue ->
    newValue > 0  // true → 변경 허용, false → 거부 (oldValue 유지)
}

positiveNumber = 10   // OK: 10 > 0
println(positiveNumber)  // 10

positiveNumber = -5   // 거부: -5 > 0 이 false
println(positiveNumber)  // 10 (변경 안 됨)

3. observable vs vetoable 비교

구분 observable vetoable
콜백 시점 변경 변경
반환 타입 Unit Boolean
변경 제어 불가 가능 (false 반환 시 거부)
주요 용도 로깅, UI 업데이트 유효성 검사, 비즈니스 규칙

4. 실전 패턴 — UI 상태 자동 갱신

class ProfileViewModel {
    var username: String by Delegates.observable("") { _, _, newValue ->
        updateUsernameUI(newValue)
    }

    var profileImage: String by Delegates.observable("") { _, _, newValue ->
        loadImage(newValue)
    }

    var followerCount: Int by Delegates.observable(0) { _, oldValue, newValue ->
        if (newValue > oldValue) {
            showFollowerAnimation()
        }
        updateFollowerCountUI(newValue)
    }
}

5. 실전 패턴 — 변경 이력 추적

class AuditedProperty<T>(initialValue: T) {
    private val history = mutableListOf<Pair<T, Long>>()

    var value: T by Delegates.observable(initialValue) { _, _, newValue ->
        history.add(newValue to System.currentTimeMillis())
    }

    fun getHistory(): List<Pair<T, Long>> = history.toList()
}

val price = AuditedProperty(1000)
price.value = 1200
price.value = 900
price.value = 1100

price.getHistory().forEach { (value, timestamp) ->
    println("$value (${java.util.Date(timestamp)})")
}
// 1200 (...)
// 900 (...)
// 1100 (...)

6. 실전 패턴 — vetoable로 비즈니스 규칙 적용

class BankAccount(initialBalance: Double) {
    var balance: Double by Delegates.vetoable(initialBalance) { _, _, newBalance ->
        if (newBalance < 0) {
            println("잔액 부족: 출금 거부 (현재: $balance, 요청 후: $newBalance)")
            false
        } else {
            true
        }
    }

    fun deposit(amount: Double) { balance += amount }
    fun withdraw(amount: Double) { balance -= amount }
}

val account = BankAccount(1000.0)
account.deposit(500.0)   // balance: 1500.0
account.withdraw(300.0)  // balance: 1200.0
account.withdraw(2000.0) // 잔액 부족: 출금 거부 (현재: 1200.0, 요청 후: -800.0)
println(account.balance) // 1200.0 (변경 안 됨)

7. 실전 패턴 — 단계 전환 제어

enum class OrderStatus {
    PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED
}

class Order {
    var status: OrderStatus by Delegates.vetoable(OrderStatus.PENDING) { _, old, new ->
        val validTransitions = mapOf(
            OrderStatus.PENDING   to setOf(OrderStatus.CONFIRMED, OrderStatus.CANCELLED),
            OrderStatus.CONFIRMED to setOf(OrderStatus.SHIPPED, OrderStatus.CANCELLED),
            OrderStatus.SHIPPED   to setOf(OrderStatus.DELIVERED),
            OrderStatus.DELIVERED to emptySet(),
            OrderStatus.CANCELLED to emptySet()
        )
        val allowed = validTransitions[old]?.contains(new) ?: false
        if (!allowed) println("유효하지 않은 상태 전환: $old → $new")
        allowed
    }
}

val order = Order()
order.status = OrderStatus.CONFIRMED  // OK
order.status = OrderStatus.DELIVERED  // 유효하지 않은 상태 전환: CONFIRMED → DELIVERED
println(order.status)                 // CONFIRMED
order.status = OrderStatus.SHIPPED    // OK
order.status = OrderStatus.DELIVERED  // OK

8. 커스텀 ObservableProperty 구현

표준 라이브러리 내부를 이해하면 더 유연한 변형을 만들 수 있습니다.

// 변경 시 여러 리스너에게 알리는 델리게이트
class MultiObservableDelegate<T>(private var value: T) {
    private val listeners = mutableListOf<(T, T) -> Unit>()

    fun addListener(listener: (old: T, new: T) -> Unit) {
        listeners.add(listener)
    }

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value

    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
        val old = value
        value = newValue
        listeners.forEach { it(old, newValue) }
    }
}

class Settings {
    val theme = MultiObservableDelegate("light")

    init {
        theme.addListener { old, new -> println("테마 변경: $old → $new") }
        theme.addListener { _, new -> saveToPrefs("theme", new) }
    }

    var themeValue: String by theme
}

9. 정리



Related Posts