
Observer 패턴과 Flow를 함께 이해하려면 먼저 누가 변화를 갖고 있고, 누가 그 변화를 구독하며, 어떤 방식으로 전달하는지를 분리해서 봐야 합니다. 이번 글은 전통적인 콜백 기반 Observer 패턴을 출발점으로 삼고, Kotlin Flow와 어디서 닮고 어디서 갈라지는지 실무 감각으로 정리합니다.
결론부터 말하면 Observer는 구독 구조를 설명하는 기본 언어이고, Flow는 그 위에 비동기 값 흐름과 연산자 모델을 얹은 더 넓은 도구입니다. 그래서 Flow는 Observer의 일부 문제를 대체할 수 있지만, 모든 observer 상황의 자동 상위호환은 아닙니다.

기본 개념 참고로는 Refactoring.Guru의 Observer 패턴 설명, Flow 공식 설명으로는 Kotlin Flows 문서, hot flow 세부 기준은 SharedFlow API와 StateFlow API를 참고했습니다.
Observer 패턴은 왜 필요할까
객체 하나의 상태가 바뀌었을 때 그 사실을 알아야 하는 대상이 하나가 아닐 때가 있습니다. 예를 들어 주문 상태가 바뀌면 화면도 갱신해야 하고, 로그도 남겨야 하고, 알림도 보낼 수 있습니다.
이때 주문 객체가 화면, 로그, 알림 클래스를 전부 직접 알면 결합도가 빠르게 올라갑니다. Observer 패턴은 subject가 변화를 소유하고 observer가 그 변화를 구독하는 구조를 만들어 이 결합을 느슨하게 합니다.
즉 subject는 누가 구독했는지만 알고, 구체적인 후속 행동은 observer 쪽으로 미룹니다. 이 단순한 분리만으로도 변경 영향 범위를 꽤 줄일 수 있습니다.
가장 전통적인 구조는 콜백 기반 Observer다
전통적인 Observer는 대체로 subject가 observer 목록을 가지고, observer는 subscribe / unsubscribe로 등록되거나 해제되며, 상태가 바뀌면 subject가 각 observer의 update를 호출하는 흐름입니다.
interface StockObserver {
fun onPriceChanged(symbol: String, newPrice: Int)
}
class StockTicker {
private val observers = mutableListOf<StockObserver>()
private var price: Int = 0
fun subscribe(observer: StockObserver) {
observers += observer
}
fun unsubscribe(observer: StockObserver) {
observers -= observer
}
fun changePrice(symbol: String, newPrice: Int) {
price = newPrice
observers.forEach { it.onPriceChanged(symbol, price) }
}
}이 구조의 장점은 눈에 잘 보인다는 점입니다. 누가 등록되고 언제 호출되는지 추적하기 쉽습니다. 반면 observer 목록 관리, 해제 누락 방지, 중복 등록 방지, 스레드 안전성 같은 책임은 직접 떠안아야 합니다.

Flow는 같은 말이 아니라 더 넓은 모델이다
여기서 가장 많이 생기는 오해는 “둘 다 구독하니까 Flow는 Observer의 코루틴 버전이다”라는 말입니다. 이 말은 절반만 맞습니다. Flow도 값을 관찰하고 collect 한다는 점에서는 구독 감각이 있지만, 공식 문서 기준으로 Flow는 비동기적으로 생산되는 sequential stream of values를 다루는 모델입니다.
즉 Flow는 알림 한 번을 보내는 도구라기보다, 값이 흘러가는 통로와 그 중간 연산을 함께 모델링합니다. 그래서 Observer와 닮아 보이면서도 훨씬 넓은 문제를 다룹니다.
- Observer는 누구에게 알릴까가 더 중심이다
- Flow는 값이 어떤 흐름으로 흘러가고 어떻게 변환될까가 더 중심이다
- 둘 다 polling보다 변화 기반 모델에 가깝지만, 운영 책임의 범위가 다르다
Observer 패턴과 Flow를 코드로 비교해보자
콜백 기반 Observer는 publisher가 observer 인터페이스를 직접 호출합니다. 반면 Flow 쪽 감각은 publisher가 값을 emit 하고, collector가 필요한 연산을 붙여서 받는 구조로 옮겨갑니다.
interface ChatObserver {
fun onMessage(message: String)
}
class ChatRoomObserverStyle {
private val observers = mutableListOf<ChatObserver>()
fun subscribe(observer: ChatObserver) {
observers += observer
}
fun unsubscribe(observer: ChatObserver) {
observers -= observer
}
fun publish(message: String) {
observers.forEach { it.onMessage(message) }
}
}
class ChatRoomFlowStyle {
private val _messages = MutableSharedFlow<String>()
val messages = _messages.asSharedFlow()
suspend fun publish(message: String) {
_messages.emit(message)
}
}
suspend fun observeChat(room: ChatRoomFlowStyle) {
room.messages
.filter { it.isNotBlank() }
.collect { println("received: $it") }
}둘 다 메시지를 받는 결과는 비슷하지만 구조는 다릅니다. 첫 번째는 subject 중심 제어 흐름이고, 두 번째는 값 흐름 중심 구조입니다. Flow는 알림만이 아니라 가공 가능한 값 파이프라인을 제공합니다.

cold Flow와 hot Flow를 모르면 비교가 자꾸 틀어진다
Kotlin 공식 문서 기준으로 일반 Flow는 cold입니다. collect가 시작될 때 생산이 시작되고, collector마다 독립 실행이 일어납니다. 이 점은 이미 살아 있는 subject에 observer가 붙는 전통적인 Observer와 꽤 다릅니다.
반대로 SharedFlow와 StateFlow는 hot flow 계열이라 Observer 감각과 훨씬 더 가까워집니다. 비교할 때 일반 Flow와 hot flow를 한데 묶어 말하면 자꾸 개념이 미끄러집니다.
- 일반 Flow: collect할 때 시작되는 cold 흐름
- SharedFlow: 여러 collector가 함께 받는 broadcast 성격의 hot 흐름
- StateFlow: 현재 상태 값을 항상 하나 들고 있는 hot 흐름
이 구분을 이해하면 왜 SharedFlow는 observer broadcast와 닮고, StateFlow는 상태 관찰에 더 잘 맞는지가 자연스럽게 보입니다.
공식 API 문서 기준으로 SharedFlow는 emitted values를 모든 collector에게 broadcast 하는 hot flow입니다. 그래서 앱 내부 이벤트 방송, 채팅 알림, 로그 브로드캐스트 같은 문제에서는 전통적인 observer 목록 수동 관리보다 더 깔끔할 수 있습니다.
StateFlow는 상태 표현에 특화된 hot flow입니다. 현재 값을 항상 들고 있고, 새 collector는 최신 값을 바로 받습니다. 그래서 UI state, 연결 상태, 화면 모델처럼 지금 상태가 무엇인지가 중요한 문제에 잘 맞습니다.
data class UiState(
val isLoading: Boolean = false,
val title: String = "",
)
class ArticleViewModel {
private val _uiState = MutableStateFlow(UiState(isLoading = true))
val uiState = _uiState.asStateFlow()
fun updateTitle(title: String) {
_uiState.value = UiState(isLoading = false, title = title)
}
}다만 StateFlow는 상태용이지 모든 이벤트 이력을 빠짐없이 전달하는 도구는 아닙니다. 공식 문서가 말하듯 updates are conflated 되므로, 느린 collector는 중간 상태를 건너뛰고 최신 값만 볼 수 있습니다.
언제 Flow가 Observer의 좋은 대체재가 될까
- 값이 한 번이 아니라 시간에 따라 계속 바뀐다
- 단순 전달뿐 아니라 필터링, 변환, 결합 같은 중간 연산이 필요하다
- 비동기 처리와 취소를 구조적으로 다뤄야 한다
- 여러 collector가 붙을 수 있고 hot/cold 특성을 의도적으로 선택해야 한다
- 현재 상태를 일관되게 공유하거나 이벤트를 브로드캐스트해야 한다
예를 들어 검색어 입력에서 debounce 뒤에 네트워크 호출을 붙이거나, 여러 데이터 소스를 combine 해서 하나의 UI state를 만들거나, 앱 내부 이벤트를 SharedFlow로 방송하는 문제에서는 Flow가 observer 수동 관리보다 더 읽기 쉽고 실수도 줄이기 쉽습니다.
이 흐름은 StateFlow와 SharedFlow 차이 글과도 직접 연결됩니다. 상태와 이벤트를 분리해 보는 감각이 여기서 특히 중요합니다.
언제 Flow가 대체재가 아니거나 과할까
버튼 클릭 하나를 즉시 콜백으로 연결하면 끝나는 문제, 사실상 구독자가 하나뿐인 단순 listener 구조, 중간 연산이나 비동기 조합이 없는 문제라면 굳이 Flow까지 갈 필요가 없습니다. 이럴 때는 인터페이스 콜백이 더 직관적일 수 있습니다.
- scope를 어디에 둘지 정해야 한다
- collect 취소 시점과 수명 관리를 신경 써야 한다
- cold/hot, replay, buffer 기본값을 이해해야 한다
- 작은 문제를 괜히 큰 구조로 만들 위험이 있다
즉 Flow는 강력한 만큼, 작은 문제를 괜히 크게 만드는 위험도 있습니다. 도입 이유가 구체적인 문제 해결이 아니라 최신 스타일 추종이라면 대개 과합니다.
안드로이드 쪽 수명 관리 실전은 repeatOnLifecycle vs launchWhenStarted, Compose 쪽은 collectAsStateWithLifecycle 정리와 함께 보면 더 현실적인 판단이 됩니다.
남용할 때 생기는 대표적인 오해
- 요즘은 다 Flow로 해야 한다고 믿는다
- 상태와 이벤트를 구분하지 않는다
- Flow를 붙이면 자동으로 구조가 좋아진다고 생각한다
- 작은 콜백도 모두 스트림으로 승격시킨다
실제로는 Flow가 구조를 대신 설계해주지 않습니다. 오히려 어떤 값이 상태인지, 어떤 값이 이벤트인지, 어느 범위에서 살아야 하는지 더 엄밀하게 정해야 합니다. 팀이 그 규칙을 공유하지 못하면 코드는 선언적으로 보이지만 읽기는 더 어려워질 수 있습니다.
코틀린 Observer 패턴을 이렇게 기억하면 오래 간다
Observer 패턴을 단순한 옛날 GUI listener 이야기로만 기억하면 아쉽습니다. 코틀린에서는 이 패턴이 Flow, SharedFlow, StateFlow를 이해하는 출발점이 됩니다. 다만 끝점은 아닙니다.
정리하면 Observer는 누가 변화를 통지하고 누가 받는지에 집중하고, Flow는 그 변화가 어떤 값 흐름으로 흘러가고 어떻게 가공되는지까지 다룹니다. 그래서 Flow는 Observer의 일부 문제를 대체할 수 있지만, 모든 observer 상황의 자동 상위호환은 아닙니다.
시리즈 흐름으로는 앞선 코틀린 디자인 패턴(18) – Memento 패턴으로 상태 저장과 복원하기, 코틀린 디자인 패턴(16) – Iterator 패턴과 sequence는 어떻게 연결될까를 함께 보면 전통 패턴을 코틀린 도구로 다시 읽는 감각이 더 또렷해집니다.
코틀린 디자인 패턴 시리즈
코틀린 디자인 패턴 시리즈(0) – 왜 아직도 디자인 패턴을 배워야 할까
코틀린 디자인 패턴(1) – Singleton 패턴은 언제 쓰고 object는 어떻게 다를까
코틀린 디자인 패턴(2) – Factory Method 패턴으로 생성 책임 나누기
코틀린 디자인 패턴(3) – Abstract Factory 패턴은 언제 필요할까
코틀린 디자인 패턴(4) – Builder 패턴과 named argument는 어떻게 다를까
코틀린 디자인 패턴(5) – Prototype 패턴과 data class copy 정리
코틀린 디자인 패턴(6) – Adapter 패턴으로 기존 코드를 새 인터페이스에 맞추기
코틀린 디자인 패턴(7) – Bridge 패턴은 상속 폭발을 어떻게 줄일까
코틀린 디자인 패턴(8) – Composite 패턴으로 트리 구조 다루기
코틀린 디자인 패턴(9) – Decorator 패턴은 상속 대신 어떻게 확장할까
코틀린 디자인 패턴(10) – Facade 패턴으로 복잡한 시스템 감추기
코틀린 디자인 패턴(11) – Flyweight 패턴은 메모리를 어떻게 아낄까
코틀린 디자인 패턴(12) – Proxy 패턴은 Decorator와 무엇이 다를까
코틀린 디자인 패턴(13) – Chain of Responsibility 패턴으로 조건 분기 줄이기
코틀린 디자인 패턴(14) – Command 패턴으로 실행과 취소를 다루는 법
코틀린 디자인 패턴(15) – Interpreter 패턴은 어디까지 써야 할까
코틀린 디자인 패턴(16) – Iterator 패턴과 sequence는 어떻게 연결될까