(Kotlin/코틀린) 행동 패턴 — 옵저버 패턴 (Observer)


1. 옵저버 패턴이란?

한 객체(Subject)의 상태 변화를 여러 객체(Observer)에게 자동으로 알리는 패턴입니다.

Subject (발행자)
    ↓ 상태 변경
    ↓ notify()
Observer1, Observer2, Observer3 ... (구독자들)

2. 기본 구현

// 옵저버 인터페이스
fun interface Observer<T> {
    fun onChanged(value: T)
}

// Subject
class Subject<T>(initialValue: T) {
    private val observers = mutableListOf<Observer<T>>()
    private var value: T = initialValue
        set(new) {
            field = new
            notifyObservers(new)
        }

    fun subscribe(observer: Observer<T>) = observers.add(observer)
    fun unsubscribe(observer: Observer<T>) = observers.remove(observer)
    fun getValue() = value
    fun setValue(new: T) { value = new }

    private fun notifyObservers(value: T) =
        observers.forEach { it.onChanged(value) }
}

// 사용
val counter = Subject(0)

val logger = Observer<Int> { println("값 변경: $it") }
val alarm  = Observer<Int> { if (it > 5) println("⚠️ 임계값 초과: $it") }

counter.subscribe(logger)
counter.subscribe(alarm)

counter.setValue(3)   // 값 변경: 3
counter.setValue(7)   // 값 변경: 7 / ⚠️ 임계값 초과: 7

counter.unsubscribe(alarm)
counter.setValue(10)  // 값 변경: 10 (alarm 없음)

3. Delegates.observable로 간결하게

import kotlin.properties.Delegates

class StockPrice {
    var price: Double by Delegates.observable(0.0) { _, old, new ->
        listeners.forEach { it(old, new) }
    }

    private val listeners = mutableListOf<(Double, Double) -> Unit>()

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

val stock = StockPrice()
stock.addListener { old, new ->
    val change = ((new - old) / old * 100)
    println("${if (change >= 0) "" else ""} ${"%.2f".format(Math.abs(change))}% ($old → $new)")
}

stock.price = 50000.0
stock.price = 52000.0  // ▲ 4.00% (50000.0 → 52000.0)
stock.price = 48000.0  // ▼ 7.69% (52000.0 → 48000.0)

4. Flow로 구현 — 현대적 방식

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

class UserRepository {
    private val _currentUser = MutableStateFlow<User?>(null)
    val currentUser: StateFlow<User?> = _currentUser.asStateFlow()

    fun login(user: User) { _currentUser.value = user }
    fun logout() { _currentUser.value = null }
}

// 구독 (ViewModel)
class ProfileViewModel(private val repo: UserRepository) : ViewModel() {
    val user = repo.currentUser  // StateFlow를 그대로 노출

    init {
        viewModelScope.launch {
            repo.currentUser.collect { user ->
                println("사용자 변경: ${user?.name ?: "로그아웃"}")
            }
        }
    }
}

5. 실전 패턴 — EventBus

object EventBus {
    private val _events = MutableSharedFlow<Any>(extraBufferCapacity = 64)
    val events = _events.asSharedFlow()

    suspend fun publish(event: Any) = _events.emit(event)

    inline fun <reified T> CoroutineScope.subscribe(
        crossinline handler: suspend (T) -> Unit
    ) = launch {
        events.filterIsInstance<T>().collect { handler(it) }
    }
}

// 이벤트 정의
data class UserLoggedIn(val userId: String)
data class CartUpdated(val itemCount: Int)

// 구독
viewModelScope.subscribe<UserLoggedIn> { event ->
    println("로그인: ${event.userId}")
}

// 발행
viewModelScope.launch {
    EventBus.publish(UserLoggedIn("user-123"))
}

6. 정리



Related Posts