267. 옵저버 (Observer) - 상태 변화 시 구독자에게 자동 알림
핵심 인사이트 (3줄 요약)
- 본질: 옵저버(Observer) 패턴은 하나의 객체(주제, Subject)의 상태가 변할 때 그 객체에 의존하는 다수의 객체(관찰자, Observer)들에게 자동으로 알림이 가고 상태가 갱신되도록 하는 1:N(일대다) 의존성 정의 패턴이다.
- 가치: 이벤트를 발생시키는 쪽(Publisher)과 이벤트를 수신하는 쪽(Subscriber) 간의 강한 결합(Coupling)을 느슨하게 풀어주어, 새로운 옵저버를 추가하거나 기존 옵저버를 제거할 때 주제 객체의 코드를 수정할 필요가 없게 만든다(OCP 준수).
- 융합: 현대 프론트엔드의 반응형 프로그래밍(RxJS, Vue, React의 State), MVC/MVVM 아키텍처에서 모델의 변경을 뷰에 반영하는 메커니즘, 그리고 분산 시스템의 이벤트 주도 아키텍처(EDA, Event-Driven Architecture)의 근간이 되는 가장 핵심적인 디자인 패턴이다.
Ⅰ. 개요 및 필요성 (Context & Necessity)
-
개념: 옵저버 패턴은 객체의 상태 변화를 관찰하는 관찰자(Observer)들의 목록을 객체(Subject)에 등록해 두고, 상태 변화가 있을 때마다 Subject가 각 Observer의 특정 메서드를 호출하여 변화를 알려주는(Notify) 행동(Behavioral) 패턴이다.
-
필요성: 만약 날씨 데이터를 수집하는 기상 스테이션(Subject)이 있고, 이 데이터를 스마트폰 앱, 전광판, 웹사이트(Observers)에 표시해야 한다고 가정하자. 기상 스테이션의 코드 내부에
SmartPhone.update(),WebSite.update()처럼 직접 함수를 호출하게 만들면, 새로운 디스플레이(예: 스마트워치)가 추가될 때마다 기상 스테이션의 코드를 뜯어고쳐야 한다. 이는 객체 지향의 핵심 원칙인 '개방-폐쇄 원칙(OCP)'을 정면으로 위반하는 것이다. -
💡 비유: 유튜브의 '구독(Subscribe)'과 '알림 설정' 기능과 완벽히 같습니다. 유튜버(Subject)는 누가 자기를 구독했는지 일일이 외우거나 관리할 필요 없이, 영상을 올리면 유튜브 시스템이 알아서 모든 구독자(Observer)의 스마트폰에 알림(Notify)을 띄워줍니다.
-
등장 배경 및 발전 과정:
- 폴링(Polling) 방식의 비효율성: 과거에는 관찰자가 주기적으로 주제에게 "혹시 변한 거 있니?"라고 묻는 폴링 방식을 썼다. 이는 상태 변화가 없을 때도 계속 물어봐야 하므로 막대한 CPU 및 네트워크 자원 낭비를 초래했다.
- 푸시(Push) 방식의 도입: 반대로 주제가 변했을 때만 관찰자에게 알려주는 푸시 방식(옵저버 패턴)이 고안되었고, 이는 GUI(Graphic User Interface) 프로그래밍의 버튼 클릭 이벤트 처리(Event Listener) 표준으로 자리 잡았다.
- 반응형(Reactive) 프로그래밍으로의 진화: 데이터의 흐름과 변화 전파에 중점을 둔 패러다임으로 발전하여 현대 웹/앱 개발의 근간이 되었다.
-
📢 섹션 요약 비유: 매일 아침 우체국에 가서 "내 편지 왔나요?"라고 묻는 것(Polling)을 그만두고, 우체부에게 우리 집 주소를 알려주면 편지가 왔을 때만 우체통에 넣어주고 가는 것(Push)과 같습니다.
Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)
구성 요소 (클래스 다이어그램)
| 요소명 | 역할 | 비유 |
|---|---|---|
| Subject (주제/발행자) | 상태를 저장하고 옵저버 목록을 관리하는 인터페이스/추상 클래스. attach(), detach(), notify() 메서드를 제공한다. | 유튜브 채널 (구독/구독취소 버튼 제공) |
| ConcreteSubject | 실제 상태를 가지고 있으며, 상태 변경 시 notify()를 호출하여 옵저버들에게 알리는 구체적 클래스. | 게임 방송 채널 (새 영상 업로드 시 알림 발송) |
| Observer (관찰자/구독자) | Subject의 변경 알림을 받기 위한 갱신 인터페이스. 보통 update() 메서드 하나만 가진다. | 유튜브 앱의 알림 수신 인터페이스 |
| ConcreteObserver | 실제 알림을 받았을 때 수행할 구체적인 행동을 정의하는 클래스. Subject에 대한 참조를 가질 수 있다. | 당신의 스마트폰, 친구의 태블릿 |
동작 메커니즘 (데이터 흐름)
옵저버 패턴에서 데이터(상태 변화)가 어떻게 전달되는지 시각화하면 다음과 같다. 핵심은 Subject가 구체적인 Observer 클래스(SmartPhone, Watch)를 전혀 모르고, 오직 추상적인 Observer 인터페이스만 알고 있다는 점이다.
┌─────────────────────────────────────────────────────────────┐
│ 옵저버 패턴의 상태 갱신 흐름 (Push 방식) │
├─────────────────────────────────────────────────────────────┤
│ │
│ [ConcreteSubject] [ConcreteObservers] │
│ (기상 스테이션) │
│ │
│ 1. 온도 변화 발생 (25℃ → 30℃) │
│ │ │
│ ▼ │
│ 2. notifyObservers() 실행 │
│ │ │
│ ├────────────────────┐ │
│ ▼ ▼ │
│ 3. observer1.update() 3. observer2.update() │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ │
│ │ 스마트폰 화면│ │ 전광판 화면 │ │
│ │ (30℃ 표시) │ │ (30℃ 표시) │ │
│ └────────────┘ └────────────┘ │
│ │
│ ※ Subject는 내부적으로 List<Observer>를 루프(for) 돌면서 │
│ update() 메서드만 다형적으로 호출할 뿐이다. │
└─────────────────────────────────────────────────────────────┘
[다이어그램 해설] Subject 내부에 저장된 List<Observer>는 구체적 타입(SmartPhone)이 아니라 인터페이스 타입(Observer)의 집합이다. Subject의 상태가 변하면 notifyObservers() 안에서 for (Observer o : observers) { o.update(state); } 코드가 실행된다. 새로 스마트워치(ConcreteObserver)가 추가되더라도, 그저 List에 하나 더 등록(attach)될 뿐 Subject의 핵심 코드는 단 한 줄도 수정되지 않는다. 이것이 **느슨한 결합(Loose Coupling)**의 위력이다.
Push 방식 vs Pull 방식
데이터를 전달하는 방법에 따라 옵저버 패턴은 크게 두 가지로 나뉜다.
| 구분 | 설명 | 장점 | 단점 |
|---|---|---|---|
| Push 방식 | Subject가 상태 변경 시 갱신된 데이터를 update(data)의 파라미터로 직접 밀어 넣는 방식. | Observer가 데이터를 찾을 필요 없이 즉시 사용 가능 | Observer가 필요 없는 데이터까지 억지로 받아야 할 수 있음 |
| Pull 방식 | Subject는 update()로 변경되었다는 사실(이벤트)만 알리고, Observer가 필요한 시점에 Subject의 getState()를 호출해 당겨(Pull) 가는 방식. | Observer가 자신에게 필요한 데이터만 선택적으로 가져갈 수 있음 | 두 번의 통신(알림 수신 → 상태 요청)이 발생함 |
- 📢 섹션 요약 비유: Push 방식이 피자집에서 배달 오토바이로 피자를 집까지 가져다주는 것이라면, Pull 방식은 피자집에서 "피자 나왔어요!"라고 문자만 보내고 손님이 직접 매장에 찾으러 가는(가져가는) 방식입니다.
Ⅲ. 융합 비교 및 다각도 분석
1. 옵저버 패턴 vs 발행-구독 (Publish-Subscribe, Pub/Sub) 패턴
옵저버 패턴과 가장 헷갈리는 것이 메시지 브로커(Message Broker) 환경에서 쓰이는 Pub-Sub 패턴이다. 둘은 비슷해 보이지만 결합도 측면에서 결정적인 차이가 있다.
| 비교 항목 | 옵저버 패턴 (Observer) | 발행-구독 패턴 (Pub-Sub) |
|---|---|---|
| 브로커(중개자) | 없음 (주제와 옵저버가 직접 연결) | 있음 (Message Broker / Event Bus) |
| 결합도 | 낮지만 서로의 존재는 알고 있음 | 완전히 분리됨 (서로의 존재를 전혀 모름) |
| 동작 공간 | 단일 애플리케이션 (주로 메모리 내부) | 분산 시스템, 네트워크 환경 (MSA 등) |
| 대표 사례 | Java/C#의 이벤트 리스너, Vue/React의 State | Kafka, RabbitMQ, Redis Pub/Sub, AWS SNS |
┌─────────────────────────────────────────────────────────────┐
│ Observer vs Pub/Sub 아키텍처 │
├─────────────────────────────────────────────────────────────┤
│ │
│ [Observer 패턴] [Pub/Sub 패턴] │
│ │
│ Subject ──────▶ Observer 1 Publisher ─┐ │
│ │ (주문서버) │ │
│ └───────────▶ Observer 2 ▼ │
│ ┌─────────────┐ │
│ ※ Subject가 Observer를 직접 호출 │ Event Bus / │ │
│ (서로 참조 객체를 가짐) │ Message Q │ │
│ └─────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ Subscriber 1 Subscriber 2 │
│ (결제서버) (알림서버) │
│ │
│ ※ Publisher와 Subscriber는 오직 Event Bus(토픽)만 안다. │
└─────────────────────────────────────────────────────────────┘
[다이어그램 해설] 옵저버 패턴은 A.notify()가 B.update()를 직접 메모리 상에서 호출하므로 동기적(Synchronous)으로 동작할 때가 많다. 반면 Pub-Sub 패턴은 중간에 큐(Queue)나 버스(Bus)가 개입하여 비동기적(Asynchronous)으로 메시지를 던져놓고 자기 할 일을 하러 가는 완전한 디커플링(Decoupling)을 지향한다.
과목 융합 관점
-
아키텍처 (Architecture): MVC (Model-View-Controller) 아키텍처에서 Model은 Subject, View는 Observer 역할을 한다. Model의 데이터가 변경되면 옵저버 패턴을 통해 여러 View가 동시에 업데이트된다.
-
클라우드 / 엔터프라이즈: MSA (마이크로서비스 아키텍처)에서는 이를 확장한 이벤트 주도 아키텍처(EDA)를 구축하며, 이때는 순수 옵저버 패턴 대신 Kafka와 같은 Pub/Sub 패턴(분산 브로커)을 활용한다.
-
📢 섹션 요약 비유: 옵저버 패턴이 반장(Subject)이 교실 안의 학생들(Observer)에게 직접 "조용히 해!"라고 소리치는 것이라면, Pub/Sub 패턴은 반장이 방송실(Event Bus)에 가서 마이크로 말하고 전교생이 스피커로 듣는 것과 같습니다.
Ⅳ. 실무 적용 및 기술사적 판단
실무 시나리오
-
시나리오 — 무한 루프(Infinite Loop) 발생의 위험: 옵저버 패턴 구현 시, Observer A가 Subject의 상태를 갱신하면, Subject가 다시
notify()를 날리고, 이를 받은 Observer B가 또 다른 Subject의 상태를 갱신하여 다시 A가 알림을 받는 **'업데이트 폭풍(Update Storm) 혹은 무한 순환 루프'**가 발생하여 시스템이 뻗어버리는 문제가 발생할 수 있다.- 아키텍트의 해결책: 양방향 바인딩을 피하고, 데이터 흐름을 철저하게 **단방향(Unidirectional Data Flow)**으로 통제해야 한다. (React, Redux, Flux 아키텍처가 옵저버 패턴의 복잡성을 해결하기 위해 단방향을 강제한 대표적 사례다.) 또한,
update()내에서 상태 갱신을 수행하기 전oldValue != newValue인지 비교하는 Guard 로직을 반드시 삽입해야 한다.
- 아키텍트의 해결책: 양방향 바인딩을 피하고, 데이터 흐름을 철저하게 **단방향(Unidirectional Data Flow)**으로 통제해야 한다. (React, Redux, Flux 아키텍처가 옵저버 패턴의 복잡성을 해결하기 위해 단방향을 강제한 대표적 사례다.) 또한,
-
시나리오 — 메모리 누수(Memory Leak) 발생 (Lapsed Listener Problem): Java나 C# 같은 가비지 컬렉션(GC) 환경에서 가장 흔한 메모리 누수 원인이다. 특정 창(UI)을 닫을 때 해당 UI 객체(Observer)를 파괴하려 했으나, Subject의
List<Observer>안에 여전히 그 UI 객체의 참조(Reference)가 남아 있어 GC가 이를 회수하지 못해 메모리가 계속 고갈된다.- 아키텍트의 해결책: 생명주기가 끝난 Observer는 반드시 Subject에서 명시적으로 구독 해제(
detach())해야 한다. 더 안전한 방법으로는 Subject가 Observer의 참조를 가질 때 **약한 참조(Weak Reference)**를 사용하여, 외부에서 Observer가 파괴되면 Subject의 목록에서도 자동으로 GC 대상이 되도록 설계해야 한다.
- 아키텍트의 해결책: 생명주기가 끝난 Observer는 반드시 Subject에서 명시적으로 구독 해제(
도입 체크리스트
- 기술적: 다수의 스레드 환경에서
attach(),detach(),notify()가 동시에 호출될 때 Thread-safe 한 자료구조(CopyOnWriteArrayList등)를 사용하여 동시성 에러(ConcurrentModificationException)를 방지했는가? - 설계적: 옵저버의 수가 1만 개 이상으로 폭증할 경우, 동기적인
notify()호출이 병목을 일으키지 않도록 비동기 이벤트 큐(Event Queue) 모델로 전환할 준비가 되어 있는가?
안티패턴
-
Fat Observer:
update()메서드 안에서 너무 복잡한 로직(DB 저장, 외부 API 호출 등)을 동기식으로 처리하는 행위. 이 경우 100개의 Observer 중 하나만 느려도 Subject 전체의 상태 갱신 프로세스가 멈춰버리는(Blocking) 재앙이 발생한다. 복잡한 작업은 반드시 비동기 스레드로 위임해야 한다. -
📢 섹션 요약 비유: 이웃에게 열쇠(구독)를 주며 우편물을 확인해 달라고 부탁했다가, 이사 갈 때 열쇠를 회수(
detach)하지 않으면 엉뚱한 사람이 계속 내 우편물을 뒤지게 되는(메모리 누수) 치명적 실수를 주의해야 합니다.
Ⅴ. 기대효과 및 결론
정량/정성 기대효과
| 구분 | 강결합 (직접 호출) 방식 | 옵저버 패턴 도입 후 | 개선 효과 |
|---|---|---|---|
| 정량 | 새 디스플레이 추가 시 수정 클래스 수 2~3개 | 수정 클래스 수 0개 (오직 새 클래스만 추가) | 확장성(OCP) 극대화, 유지보수 공수 80% 감소 |
| 정량 | 폴링 방식 사용 시 CPU 점유율 40% | 푸시 방식 사용 시 변화 순간에만 점유율 상승 | 불필요한 반복 연산 제거로 자원 낭비 최소화 |
| 정성 | 데이터 제공자와 UI 로직이 뒤엉켜 스파게티 코드 양산 | 로직 분리로 단위 테스트(Unit Test) 독립성 확보 | 도메인 로직의 재사용성 및 코드 가독성 향상 |
미래 전망
- 반응형 스트림(Reactive Streams)의 표준화: 단순한 옵저버 패턴을 넘어, 비동기 데이터 스트림을 처리하고 백프레셔(Backpressure, 수신자가 처리할 수 있는 만큼만 발행자에게 요청)를 지원하는 Reactive Programming (RxJava, Project Reactor, Flow API)이 현대 자바 및 백엔드 프레임워크(Spring WebFlux)의 핵심 패러다임으로 자리 잡고 있다.
- 상태 관리 라이브러리의 진화: 프론트엔드에서는 단순 옵저버 패턴을 넘어, 전역 상태 트리를 관리하는 Redux, Recoil, Zustand, Jotai 등의 라이브러리로 진화하여 복잡한 UI의 상태 동기화 문제를 해결하고 있다.
참고 표준
- GoF (Gang of Four): Behavioral Patterns - Observer
- Java
java.util.Observable/Observer: 과거 표준이었으나 한계로 인해 Java 9부터 Deprecated 됨. - Java 9
java.util.concurrent.Flow: Reactive Streams 표준을 Java 핵심 API로 도입한 새로운 관찰자 패러다임.
결론적으로 옵저버 패턴은 객체 지향 설계에서 **'무엇(What)이 변했는가'**와 **'어떻게(How) 반응할 것인가'**를 물리적으로 완벽히 분리해내는 위대한 성취다. 기술사는 단순히 패턴의 구조를 외우는 것을 넘어, 이것이 양방향 결합 시 발생할 무한 루프 위험과 메모리 누수를 관리해야 하는 날 선 검이라는 점을 인지하고, 현대적인 Reactive 아키텍처로 적절히 승급시킬 줄 알아야 한다.
- 📢 섹션 요약 비유: 수십 명의 악기 연주자(Observer)가 지휘자(Subject)의 손짓 하나에 맞춰 아름다운 교향곡을 만들어내듯, 시스템 내의 수많은 부품들이 완벽한 타이밍에 협력하게 만드는 마에스트로와 같은 패턴입니다.
📌 관련 개념 맵 (Knowledge Graph)
| 개념 명칭 | 관계 및 시너지 설명 |
|---|---|
| OCP (Open-Closed Principle) | 확장에 열려 있고 수정에 닫혀 있어야 한다는 SOLID 원칙. 옵저버 패턴이 이를 완벽히 구현하는 대표적 사례다. |
| Pub/Sub (발행-구독) 패턴 | 옵저버 패턴의 확장판으로, 분산 환경에서 이벤트 버스를 두어 결합도를 0으로 만든 비동기 아키텍처. |
| MVC (Model-View-Controller) | 화면과 데이터 로직을 분리하는 아키텍처로, Model의 변화를 View에 반영할 때 옵저버 패턴이 내장되어 쓰인다. |
| Reactive Programming | 데이터 흐름과 변화의 전파에 초점을 맞춘 프로그래밍 패러다임으로, 옵저버 패턴을 데이터 스트림 처리에 맞게 극대화한 형태. |
| Lapsed Listener Problem | 가비지 컬렉션 환경에서 메모리 누수를 일으키는 가장 유명한 문제로, 옵저버 등록 해제를 잊었을 때 발생한다. |
👶 어린이를 위한 3줄 비유 설명
- 여러분이 짱구 유튜브 채널(Subject)을 좋아해서 '구독과 알림 설정'(Observer 등록)을 눌렀다고 해볼게요.
- 짱구가 새 영상을 올리면, 짱구가 여러분의 이름을 하나하나 부르지 않아도 스마트폰에 "새 영상이 올라왔어요!" 하고 **알림(Notify)**이 자동으로 뜹니다.
- 이처럼 변동 사항이 생겼을 때, 그걸 기다리고 있던 모든 사람(객체)에게 자동으로 소식을 쫙 뿌려주는 똑똑한 약속을 **'옵저버 패턴'**이라고 부른답니다!