267. 옵저버 (Observer) - 상태 변화 시 구독자에게 자동 알림

핵심 인사이트 (3줄 요약)

  1. 본질: 옵저버(Observer) 패턴은 하나의 객체(주제, Subject)의 상태가 변할 때 그 객체에 의존하는 다수의 객체(관찰자, Observer)들에게 자동으로 알림이 가고 상태가 갱신되도록 하는 1:N(일대다) 의존성 정의 패턴이다.
  2. 가치: 이벤트를 발생시키는 쪽(Publisher)과 이벤트를 수신하는 쪽(Subscriber) 간의 강한 결합(Coupling)을 느슨하게 풀어주어, 새로운 옵저버를 추가하거나 기존 옵저버를 제거할 때 주제 객체의 코드를 수정할 필요가 없게 만든다(OCP 준수).
  3. 융합: 현대 프론트엔드의 반응형 프로그래밍(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)을 띄워줍니다.

  • 등장 배경 및 발전 과정:

    1. 폴링(Polling) 방식의 비효율성: 과거에는 관찰자가 주기적으로 주제에게 "혹시 변한 거 있니?"라고 묻는 폴링 방식을 썼다. 이는 상태 변화가 없을 때도 계속 물어봐야 하므로 막대한 CPU 및 네트워크 자원 낭비를 초래했다.
    2. 푸시(Push) 방식의 도입: 반대로 주제가 변했을 때만 관찰자에게 알려주는 푸시 방식(옵저버 패턴)이 고안되었고, 이는 GUI(Graphic User Interface) 프로그래밍의 버튼 클릭 이벤트 처리(Event Listener) 표준으로 자리 잡았다.
    3. 반응형(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의 StateKafka, 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)에 가서 마이크로 말하고 전교생이 스피커로 듣는 것과 같습니다.


Ⅳ. 실무 적용 및 기술사적 판단

실무 시나리오

  1. 시나리오 — 무한 루프(Infinite Loop) 발생의 위험: 옵저버 패턴 구현 시, Observer A가 Subject의 상태를 갱신하면, Subject가 다시 notify()를 날리고, 이를 받은 Observer B가 또 다른 Subject의 상태를 갱신하여 다시 A가 알림을 받는 **'업데이트 폭풍(Update Storm) 혹은 무한 순환 루프'**가 발생하여 시스템이 뻗어버리는 문제가 발생할 수 있다.

    • 아키텍트의 해결책: 양방향 바인딩을 피하고, 데이터 흐름을 철저하게 **단방향(Unidirectional Data Flow)**으로 통제해야 한다. (React, Redux, Flux 아키텍처가 옵저버 패턴의 복잡성을 해결하기 위해 단방향을 강제한 대표적 사례다.) 또한, update() 내에서 상태 갱신을 수행하기 전 oldValue != newValue인지 비교하는 Guard 로직을 반드시 삽입해야 한다.
  2. 시나리오 — 메모리 누수(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 대상이 되도록 설계해야 한다.

도입 체크리스트

  • 기술적: 다수의 스레드 환경에서 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줄 비유 설명

  1. 여러분이 짱구 유튜브 채널(Subject)을 좋아해서 '구독과 알림 설정'(Observer 등록)을 눌렀다고 해볼게요.
  2. 짱구가 새 영상을 올리면, 짱구가 여러분의 이름을 하나하나 부르지 않아도 스마트폰에 "새 영상이 올라왔어요!" 하고 **알림(Notify)**이 자동으로 뜹니다.
  3. 이처럼 변동 사항이 생겼을 때, 그걸 기다리고 있던 모든 사람(객체)에게 자동으로 소식을 쫙 뿌려주는 똑똑한 약속을 **'옵저버 패턴'**이라고 부른답니다!