Observer Pattern
Subject의 상태 변화를 attach된 Observer들에게 통지하는 일대다 관계 패턴. 양쪽이 인터페이스로만 알고 느슨하게 결합된다.
난이도 입문 · 선행 없음
한 줄 요약
어떤 객체의 상태가 바뀌면 거기 등록된 다른 객체들에게 자동으로 알린다. “이 값이 바뀌면 저쪽도 갱신해야 해”라는 일대다 관계가 보이면 Observer.
어떤 문제를 푸는가
주식 가격이 바뀔 때 화면·알림·로그를 갱신해야 한다. 단순하게 짜면 Stock이 모두를 직접 알아야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
class Stock {
int price;
PriceDisplay& display;
AlertService& alert;
PriceLogger& logger;
public:
void setPrice(int p) {
price = p;
display.refresh(p);
alert.checkThreshold(p);
logger.record(p);
}
};
문제:
Stock이 디스플레이·알림·로거를 다 알게 된다 (방향이 거꾸로). 관계없는 책임이 모두 Stock에 결합된다.- 새 구독자(웹훅, 메트릭 수집기) 추가할 때마다
Stock을 수정해야 한다 (OCP 위반). - 테스트할 때
Stock을 띄우려면 모든 의존성을 들고 와야 한다.
패턴 적용 후
Stock은 “관심 있는 자가 등록해라” 인터페이스만 노출한다. 구독자는 인터페이스만 구현한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <algorithm>
#include <functional>
#include <iostream>
#include <memory>
#include <vector>
class Observer {
public:
virtual void onPriceChanged(int newPrice) = 0;
virtual ~Observer() = default;
};
class Stock {
int price = 0;
std::vector<Observer*> observers; // 비소유 참조
public:
void attach(Observer* o) { observers.push_back(o); }
void detach(Observer* o) {
observers.erase(std::remove(observers.begin(), observers.end(), o),
observers.end());
}
void setPrice(int p) {
price = p;
for (auto* o : observers) o->onPriceChanged(p);
}
};
class PriceDisplay : public Observer {
public:
void onPriceChanged(int p) override {
std::cout << "[화면] 현재가: " << p << std::endl;
}
};
class AlertService : public Observer {
int threshold;
public:
AlertService(int t) : threshold(t) {}
void onPriceChanged(int p) override {
if (p >= threshold) std::cout << "[알림] " << threshold << " 돌파!" << std::endl;
}
};
int main() {
Stock stock;
PriceDisplay display;
AlertService alert(70000);
stock.attach(&display);
stock.attach(&alert);
stock.setPrice(65000);
stock.setPrice(72000);
stock.detach(&alert);
stock.setPrice(80000); // 알림은 더이상 안 울림
}
달라진 점:
Stock은Observer인터페이스만 안다. 구체 클래스를 모른다.- 새 구독자 추가 = 새 클래스 +
attach.Stock무손상. - 구독자 입장에서도 Stock의 내부를 알 필요가 없다.
구조
1
2
3
4
5
Subject ──attach/detach/notify──▶ Observer (interface)
│ ▲
│ │
▼ ┌──────────┴──────────┐
ConcreteSubject ConcreteObserverA ConcreteObserverB
- Subject: 등록·해제·통지 인터페이스 (
Stock) - Observer: 통지받는 인터페이스 (
Observer) - ConcreteSubject: 상태를 가진 실체
- ConcreteObserver: 통지 처리 (
PriceDisplay,AlertService)
Push vs Pull
| Push | Pull | |
|---|---|---|
| 형태 | onPriceChanged(int p) | onChanged(Stock& s) 후 Observer가 s.getPrice() 호출 |
| 장점 | 호출 한 번, 단순 | Observer가 필요한 데이터만 골라 가져감 |
| 단점 | Subject가 무엇을 줄지 결정 — 미리 정의 못한 정보는 못 전달 | 호출 횟수 증가, Observer가 Subject에 더 의존 |
전달할 데이터가 1~2개로 명확하면 Push, 어떤 게 필요할지 모르면 Pull.
함수형 변형 — 람다 구독
Observer 인터페이스 대신 콜백 함수만 받아도 같은 효과. 클래스를 만들 가치가 없을 때 유용.
1
2
3
4
5
6
7
8
9
10
11
12
class Stock {
int price = 0;
std::vector<std::function<void(int)>> listeners;
public:
void subscribe(std::function<void(int)> fn) { listeners.push_back(std::move(fn)); }
void setPrice(int p) {
price = p;
for (auto& f : listeners) f(p);
}
};
stock.subscribe([](int p) { std::cout << "현재가: " << p << std::endl; });
단, 람다는 detach를 표현하기 어렵다. 해제가 필요하면 토큰을 반환하는 방식으로 설계.
실전 사례
- DOM 이벤트:
element.addEventListener('click', handler)— 가장 친숙한 Observer. - Spring
ApplicationEvent:@EventListener로 이벤트 구독. - Reactive Programming (RxJS, Reactor): Observer를 시간축으로 확장한 형태.
- Vue/React 반응성: 내부적으로 의존성 추적 + Observer 발상.
Observer vs Pub/Sub
종종 같은 의미로 쓰이지만 구별하면:
| Observer (GoF) | Pub/Sub | |
|---|---|---|
| 결합 | Subject가 Observer 리스트를 직접 보유 | 중간 broker(이벤트 버스)가 있음 |
| 통지 시점 | 동기, 같은 프로세스 | 보통 비동기, 프로세스/네트워크 너머 가능 |
| 토픽 | 보통 없음 (1개 Subject에 묶임) | 토픽/채널로 분류 |
| 예 | DOM 이벤트, Spring ApplicationEvent | Kafka, Redis Pub/Sub |
같은 발상이 분산 시스템 규모로 확장된 게 Pub/Sub. GoF Observer는 단일 프로세스 내의 클래스 결합 문제를 푼다.
안티패턴 / 주의
- 무한 루프 / 캐스케이드: A → B → A 식 상호 통지가 생기면 스택 오버플로. 누가 누구를 깨우는지 그래프로 그려두는 게 안전.
- Observer가 죽었는데 detach 안 하면 댕글링. 비소유 포인터를 보유할 땐 소멸 시점에 반드시 해제, 또는 약한 참조(
weak_ptr) 사용. - 콜백 안에서 attach/detach 호출: 리스트를 순회 중인데 수정되면 UB. 큐로 미루거나 사본 순회로 처리.
- 알림 폭주: 한 번에 1000개 Observer에 동기 호출 = setPrice가 멈춘다. 빈도 높은 갱신은 배치·디바운스를 고려.
스스로 점검
1. Stock에 100명이 구독 중인데 setPrice가 멈춘다. 무엇이 문제인가?
답
동기 호출이 직렬화돼서. 100명 통지가 끝날 때까지 setPrice가 블로킹. 빈도 높은 갱신은 비동기·배치·디바운스 고려.
2. A → B → A로 서로 통지하는 옵저버 관계가 생기면?
답
무한 루프 → 스택 오버플로. Observer 그래프에 사이클이 없어야 한다. “누가 누구를 깨우는가”를 그래프로 그려보는 습관.
3. GoF Observer와 Kafka 같은 Pub/Sub의 가장 큰 차이는?
답
중간 broker의 유무. Pub/Sub은 Publisher와 Subscriber가 서로 모른다(broker가 중계). Observer는 Subject가 Observer 리스트를 직접 보유. 같은 발상이 분산 환경으로 확장된 형태.