23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
이번 포스팅에서는 지난 프로젝트에서 겪은 몇몇 경험과 책을 통해 정리한 내용을 공유하려 한다.
이전 글에서 StateFlow로 RxJava를 대체한 부분을 간략히 다루었는데, 이번에는 Coroutine과 Flow를 활용하여 Observer 패턴의 메커니즘을 다루고자 한다.
이 글은 실질적인 코드보다는 개념 이해를 위한 해설이다.
RxJava와 같은 Reactive Programming을 접하며 Observer 패턴에 관해 관심이 생겼으나, 아직 이해가 부족한 분들께 도움이 되길 바란다.
Observer 패턴은 객체의 상태 변화를 감지하고 이를 다른 객체들에 알리는 디자인 패턴이다.
이번 글에서는 Coroutine과 Flow를 통해 어떻게 더 간결하고 효율적으로 Observer 패턴을 구현할 수 있는지 알아보자.
또한, 이러한 접근법이 기존의 방법에 비해 어떤 장점을 가지는지에 대해서도 다룰 것이다.
이 글을 통해 Coroutine과 Flow의 기본 개념을 이해하고, 이를 활용하여 Observer 패턴을 구현하는 방법을 배울 수 있을 것이다.
옵저버 패턴은 객체의 상태 변화를 관찰하는 관찰자들,
즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴이다.
주로 분산 이벤트 핸들링 시스템을 구현하는 데 사용된다. 발행/구독 모델로 알려져 있기도 하다.
쉽게 말하자면, 옵저버 패턴은 객체 간의 일-대-다 종속성을 토대로 특정 객체의 변화를 여러 다른 객체들이 자동으로 감지하고 반응하는 패턴이다.
예를 들어, 유튜브 구독 시스템을 생각해 보자.
유튜브 채널(Observable)에서 새로운 비디오(Event)가 업로드되고(발행되고), 그걸 유튜브 채널(Publisher)을 구독한 구독자(Subscriber)들이 수신하는 상황이다.
이처럼 발행할 비디오(Event)를 가진 객체를 발행자라고 부르고, 해당 객체의 변화를 전달받기를 희망하는 객체들을 구독자라고 부른다.
더불어 발행자와 구독자를 부르는 명칭은 예제 코드마다 다르다.
본 포스팅에서는 발행자를 Subject, 구독자를 Observer라 부르려 한다.
안드로이드 개발에서 옵저버 패턴은 종종 브로드캐스트 메커니즘과 혼동될 수 있다.
두 패턴 모두 이벤트 기반 통신을 제공하지만, 중요한 차이가 있다.
범위
옵저버 패턴: 주로 앱 내부의 컴포넌트 간 통신에 사용된다.
브로드캐스트: 시스템 전체 또는 앱 간의 메시지 전달에 사용된다.
결합도
옵저버 패턴: Publisher와 Observer 간에 직접적인 관계가 있다.
브로드캐스트: 송신자와 수신자 간에 느슨한 결합이 있습니다.
성능
옵저버 패턴: 직접적으로 메서드 호출하여 일반적으로 더 효율적이다.
브로드캐스트: 시스템을 통한 전파로 인해 상대적으로 오버헤드가 큽니다.
위와 같은 이유로,
옵저버 패턴은 실시간 데이터 동기화, UI 업데이트 등에 적합하고, 브로드 캐스트는 시스템 이벤트 알림, 앱 간 통신 등에 적합하다.
브로드 캐스트 개념과의 차이를 이해하고 나면 우리가 알아볼 옵저버 패턴이 어떤 것인지 느낌이 올 것이다.
본 포스팅에서 이 개념에 대해서 알아가고, 어떠한 메커니즘으로 동작하는지 파헤쳐볼 것이다.
최신 안드로이드 개발에서는 Coroutine과 Flow를 활용하여 이러한 옵저버 패턴을 더 효율적이고 유연하게 구현할 수 있다.
Flow는 비동기적으로 계산되는 데이터 스트림을 표현하며, 이는 옵저버 패턴의 자동 감지 및 반응 메커니즘을 우아하게 구현할 수 있게 해주기 때문이다.
이제 우리는 본격적으로 옵저버 패턴을 마주할 준비를 마쳤다.
어떻게 옵저버 패턴이 구성되어 있어서 일-대-다 종속성을 정의할 수 있을까?
천천히 뜯어 보자.
안드로이드 애플리케이션 개발에서 데이터의 변화를 실시간으로 반영하는 것은 매우 중요하다.
UI 업데이트, 네트워크 상태 모니터링, 데이터 동기화 등 다양한 상황에서 옵저버 패턴이 활용된다.
이 패턴의 핵심 구성 요소를 살펴보며 각 요소가 어떻게 구성되어 있는지 알아보자.
옵저버 패턴은 객체 간의 일-대-다 의존 관계를 정의하는 디자인 패턴이다.
이 패턴은 주로 데이터의 변화를 여러 객체에 알려야 할 때 사용된다.
유튜브 구독 시스템을 예로 들어 옵저버 패턴의 핵심 구성 요소를 살펴보자.
Subject(발행자)는 관찰 대상이 되는 데이터나 객체를 의미한다. 대게 다음과 같은 형태로 나타날 수 있다.
Subject의 주요 책임은 Observer 들의 목록을 관리하고 상태 변경 시 모든 Observer에게 알림을 주는 것이다.
interface Subject {
fun registerObserver(observer: Observer)
fun unregisterObserver(observer: Observer)
fun notifyObservers()
}
class YoutubeChannel(private val channelName: String): Subject {
private val subscribers = mutableListOf<Observer>()
override fun subscribe(observer: Observer) {
subscribers.add(observer)
println("${observer.name}님이 $channelName 채널을 구독했습니다.")
}
override fun unsubscribe(observer: Observer) {
subscribers.remove(observer)
println("${observer.name}님이 $channelName 채널 구독을 취소했습니다.")
}
override fun notifyObservers(videoTitle: String) {
subscribers.forEach { it.update(channelName, videoTitle) }
}
fun uploadVideo(videoTitle: String) {
println("$channelName 채널에 새 영상이 업로드되었습니다: $videoTitle")
notifyObservers(videoTitle)
}
}옵저버 (Observer) 또는 구독자(Subscriber) 옵저버는 Subject의 변경 사항에 반응하는 객체이다.
주요 책임으로는 Subject로부터 업데이트를 받아 처리하는 메서드를 구현한다.
interface Observer {
val name: String
fun update(channelName: String, videoTitle: String)
}
class Subscriber(override val name: String) : Observer {
override fun update(channelName: String, videoTitle: String) {
println("$name님, $channelName 채널에 새 영상이 업로드되었습니다: $videoTitle")
}
}옵저버 패턴의 동작 과정을 유튜브 구독 시스템 예시를 통해 살펴보자.
fun main() {
// 유튜브 채널(Subject) 생성
val techChannel = YouTubeChannel("테크 리뷰 채널")
val cookingChannel = YouTubeChannel("요리 레시피 채널")
// 구독자(Observer) 생성
val alice = Subscriber("Alice")
val bob = Subscriber("Bob")
val charlie = Subscriber("Charlie")
// 구독 신청
techChannel.subscribe(alice)
techChannel.subscribe(bob)
cookingChannel.subscribe(bob)
cookingChannel.subscribe(charlie)
// 새 영상 업로드 (상태 변경)
techChannel.uploadVideo("최신 스마트폰 리뷰")
cookingChannel.uploadVideo("간단한 파스타 레시피")
// 구독 취소
techChannel.unsubscribe(alice)
// 다시 새 영상 업로드
techChannel.uploadVideo("새로운 노트북 비교")
}이 예시에서 옵저버 패턴의 동작 과정은 다음과 같다.
초기 설정: Subject(유튜브 채널)와 Observer(구독자)가 생성된다.
구독: Observer들이 관심 있는 Subject를 구독합니다. 이때 Subject의 내부 리스트에 Observer가 추가된다.
상태 변경: Subject의 상태가 변경된다 (새 영상 업로드).
알림: Subject는 자신의 상태 변경을 모든 등록된 Observer에게 알린다.
업데이트: 각 Observer는 받은 알림에 따라 자신의 상태를 업데이트한다. (새 영상 정보 출력).
구독 취소: Observer가 더 이상 알림을 받고 싶지 않을 때 구독을 취소할 수 있다.
옵저버 패턴에서 Observer(구독자)는 Observer 인터페이스를 구현하여 생성된다.
이 Observer 인터페이스는 YouTubeChannel(Subject)과 Observer 인스턴스 사이의 추상화된 연결 다리 역할을 한다.
이러한 추상화를 통해 YouTubeChannel과 Observer는 서로의 구체적인 구현을 알 필요 없이 상호작용할 수 있다.
Observer 인터페이스의 핵심은 update()메서드다.
이 메서드는 Subject의 상태 변경을 Observer에게 통지하는 역할을 한다.
update()메서드는 다음과 같은 특징을 갖는다.
먼저, 상태 갱신에 대한 역할을 수행하는데 Observer가 Subject의 상태 변화를 반영할 수 있도록 한다.
Subject가 변경되면 Observer는 update()메서드를 통해 그 변경 사항을 전달받게 된다.
그리고 추상화의 역할도 수행한다. update()메서드는 옵저버 패턴의 핵심으로, Subject와 Observer 간의 느슨한 결합을 가능하게 한다.
Subject는 단순히 update()메서드를 호출함으로써 Observer에게 상태 변화를 통지한다.
Subject는 구체적으로 어떤 Observer가 있는지, 어떻게 상태를 반영하는지 알 필요가 없다.
마지막으로, 다형성이다. Observer 인터페이스를 구현하는 다양한 구체 클래스들은 각기 다른 방식으로 update() 메서드를 구현할 수 있다.
예를 들어, 새로운 유형의 구독자(예: PremiumSubscriber)를 추가하고 싶다면, 단순히 Observer 인터페이스를 구현하는 새 클래스를 만들면 된다.
기존의 YouTube 채널 코드는 전혀 수정할 필요가 없다.
class PremiumSubscriber(override val name: String) : Observer {
override fun update(channelName: String, videoTitle: String) {
println("프리미엄 구독자 $name님, $channelName 채널의 새 영상을 먼저 시청하세요: $videoTitle")
}
}또한, YouTubeChannel은 subscribe() 메서드를 통해 자신이 발행할 이벤트(새 동영상)의 수신을 원하는 Observer 인스턴스들을 저장한다.
새 동영상이 업로드되면 (uploadVideo() 메서드를 통해), YouTubeChannel은 저장된 모든 Observer의 update() 메서드를 호출하여 구독자들에게 변경 사항을 알린다.
다시 말해, YouTubeChannel은 이렇게 말할 수 있다:
“새 동영상이 업로드되면, 내가 가진 모든 observer들의 update() 메서드를 호출해서 새 동영상 정보를 전달할거야."
YouTubeChannel이 subscrubers를 가지게 되는 시점은 subscribe()메서드가 호출될 때 이다.
fun subscribe(subscriber: Subscriber) {
subscribers.add(subscriber)
}실제 구현을 다시 한번 살펴보면:
val techChannel = YouTubeChannel("테크 리뷰 채널")
val alice = Subscriber("Alice")
val bob = Subscriber("Bob")
techChannel.subscribe(alice)
techChannel.subscribe(bob)
techChannel.uploadVideo("최신 스마트폰 리뷰")이 코드에서 우리는 다음과 같은 옵저버 패턴의 핵심 동작을 볼 수 있다.
YouTubeChannel(techChannel)은 Subscriber타입의 리스트(subscribers)에 구독자들(alice, bob)을 저장한다.
새 동영상이 업로드되면
YouTubeChannel은 저장된 모든 Subscriber들의 update()메서드를 호출하여 새 동영상 정보를 전달한다.
YouTubeChannel은 자신의 subscribers(구독자들)에게 새 동영상 정보를 전달해 주기 위해 실행하는 update() 메서드의 구체적인 구현 내용을 모른다.
이는 옵저버 패턴의 핵심적인 특징이다.
YouTubeChannel이 실행할 update() 메서드는 Subscriber가 정의한 것이다.
그렇다면 왜 YouTubeChannel이, Subscriber의 메서드를 실행해야 할까?
그 이유는 간단하다. YouTuebeChannel이 새 동영상 업로드 시점을 알고 있지만, 해당 정보를 원하는 Subscriber는 그 시점을 모르기 때문이다.
따라서 옵저버 패턴의 동작 메커니즘은 다음과 같다:
새 동영상 업로드 시점을 아는 YouTubeChannel이, Subscriber들이 새 동영상 정보를 전달 받아 수행하고자 하는 행동을 가지고 있다가, 업로드 시점에 대신 실행 해준다.
간단히 말해, 새 동영상이 업로드 돠면 Subscriber가 정의한 동작을 실행한다.
각 구독자가 같은 이벤트(새 동영상 업로드)에 대해 다르게 반응하고 싶을 수 있다.
예를 들어,
Subscriber1: “나는 새 동영상이 업로드되면 알림을 받아야 해”
Subscriber2: “나는 새 동영상을 보고 리뷰를 작성해야 해”
이런 경우,
update()메서드의 내용을 Subscriber 클래스 내부에 고정할 수 없다.그렇다면 어떻게 해야 할까?
일단, 지금까지 우리가 알고 있는 정보를 정리해 보자.
YouTubeChannel은 새 동영상 업로드 시점은 알지만, 구독자들이 그 정보로 무엇을 할지는 모른다.
Subscriber는 새 동영상 정보로 하고 싶은 일이 있지만, 업로드 시점을 모른다.
이러한 관계를 고려할 때, YouTubeChannel은 Subscriber에 행동을 전달받아, 적절한 시점에 대신 실행해 주는 역할을 한다.
즉, YouTubeChannel은 이렇게 말할 수 있다.
”내가 새 동영상이 업로드될 때 대신 실행시켜 줄 테니까, 구독할 때 새 동영상 정보를 받아서 하고 싶은 일을 리스너로 넘겨줘”
이러한 개념을 Kotlin과 Coroutine, Flow를 사용하여 구현하면 다음과 같다.
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
class YouTubeChannel(val name: String) {
private val _videos = MutableSharedFlow<String>()
val videos = _videos.asSharedFlow()
suspend fun uploadVideo(videoTitle: String) {
println("$name 채널에 새 영상이 업로드되었습니다: $videoTitle")
_videos.emit(videoTitle)
}
}
fun main() = runBlocking {
val techChannel = YouTubeChannel("테크 리뷰 채널")
val scope = CoroutineScope(Dispatchers.Default)
// Subscriber1
scope.launch {
techChannel.videos.collect { videoTitle ->
println("Subscriber1: 새 동영상 알림을 받았습니다 - $videoTitle")
}
}
// Subscriber2
scope.launch {
techChannel.videos.collect { videoTitle ->
println("Subscriber2: 새 동영상 리뷰를 작성합니다 - $videoTitle")
}
}
techChannel.uploadVideo("최신 스마트폰 리뷰")
delay(100) // 비동기 처리를 위한 약간의 지연
}이 구현에서 YouTubeChannel은 _Flow_을 사용하여 이벤트(새 동영상)를 발행한다.
각 Subscriber는 이 _Flow_을 구독하고, 자신만의 방식으로 이벤트를 처리한다.
이를 통해 옵저버 패턴의 핵심인 “YouTubeChannel은 Subscriber의 구체적인 구현을 모르면서도 이벤트를 전달할 수 있다"는 개념을 유지하면서,
동시에 각 Subscriber가 원하는 대로 이벤트를 처리할 수 있는 유연성을 제공한다.
이러한 접근 방식은 _Coroutine_과 _Flow_의 장점을 활용하여 비동기적이고 효율적인 옵저버 패턴 구현을 가능하게 한다.
또한, 백프레셔 처리, 취소 가능성, 스레드 안전성 등 다양한 고급 기능을 쉽게 추가할 수 있는 확장성도 제공한다.
자, 지금까지 옵저버 패턴에 대해 살펴봤다.
근데 이 패턴, 정말 쓸 만한 걸까? 한번 객체지향의 관점에서 들여다보자.
옵저버 패턴은 객체지향 프로그래밍의 핵심 원칙들을 아주 잘 반영하고 있다.
캡슐화의 달인: Subject와 Observer 각자가 자기 할 일만 꼭꼭 숨겨두고 있다. 서로의 비밀은 건들지 않으면서도 잘 협력한다.
다형성의 마술: 하나의 Subject에 여러 종류의 Observer들이 구독할 수 있다. 마치 한 유튜브 채널에 다양한 시청자들이 있는 것처럼 말이다.
의존성 역전의 묘기: Subject는 구체적인 Observer 클래스를 몰라도 된다. 그저 Observer 인터페이스만 알면 된다. 이거야말로 진정한 '눈감고 코끼리 그리기' 아닐까? 싶다.
재사용성의 향연: Subject와 Observer는 서로 독립적이다. 마치 레고 블록처럼 여기저기 갖다 붙일 수 있다.
확장성의 끝판왕: Observer를 추가하든 삭제하든 Subject는 눈 하나 깜짝하지 않는다. "새 친구가 왔네? 어서 와~” 정도?
이런 특징들 덕분에 옵저버 패턴은 객체 간의 의존성은 줄이면서도 유연하게 확장할 수 있는 구조를 만들어준다.
그래서 어디에 쓸 수 있을까?
실시간으로 데이터를 주고받아야 하는 상황
UI 업데이트가 필요한 경우
이벤트 기반 시스템 구축
분산 시스템에서의 상태 동기화
등등 다양한 상황에서 활용할 수 있다.
결론적으로, 옵저버 패턴은 객체지향의 정수를 보여주는 패턴이라고 할 수 있다.
복잡한 시스템을 단순하게 만들어주고, 변화에 유연하게 대응할 수 있게 해주기 때문이다.
지금까지 옵저버 패턴을 가볍게 맛보고 돌아왔다.
이를 통해 우리는 객체들이 어떻게 서로를 은밀하게 지켜보며 효율적으로 소통하는지 알게 되지 않았나 싶다.
마치 우리가 좋아하는 유튜버의 새 영상 알림을 기다리는 것처럼 말이다.
이제 우리는 세상을 옵저버의 시각으로 바라볼 수 있게 되었다. 세상의 모든 이벤트를 주시하고, 필요할 때 적절히 반응할 준비가 된 것이다.
커피숍에서 바리스타가 "아메리카노 한 잔 나왔습니다!"라고 외치는 순간,
그것이 바로 실생활의
notify()메서드임을 떠올려 보라! 우리의 이름을 들었을 때만 반응하는 것, 그것이 바로 우리의update()메서드다.
옵저버 패턴은 단순히 코드 구조를 설명하는 것이 아니다.
우리의 일상에서 끊임없이 일어나는 상호작용을 우아하게 모델링한 방식이다. 코드에서도 이런 우아한 객체 간의 상호작용을 구현해 보는 건 어떨까?
다음에 새로운 기능을 설계할 때, 잠시 멈추고 생각해 보라.
"이걸 옵저버 패턴으로 구현하면 어떨까?"
아마도 코드가 더욱 유연하고, 확장할 수 있으며, 무엇보다도 흥미로워지지 않을까 생각된다.
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.