포스트

Observer Pattern

Subject의 상태 변화를 attach된 Observer들에게 통지하는 일대다 관계 패턴. 양쪽이 인터페이스로만 알고 느슨하게 결합된다.

Observer Pattern

난이도 입문 · 선행 없음

한 줄 요약

어떤 객체의 상태가 바뀌면 거기 등록된 다른 객체들에게 자동으로 알린다. “이 값이 바뀌면 저쪽도 갱신해야 해”라는 일대다 관계가 보이면 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); // 알림은 더이상 안 울림
}

달라진 점:

  • StockObserver 인터페이스만 안다. 구체 클래스를 모른다.
  • 새 구독자 추가 = 새 클래스 + 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

 PushPull
형태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 ApplicationEventKafka, 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 리스트를 직접 보유. 같은 발상이 분산 환경으로 확장된 형태.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.