728x90
320x100
1. Observer Pattern?
- 정의
- 한 객체(Subject)의 상태가 바뀌면, 그 객체에 의존하는 여러 옵저버(Observers)들이 자동으로 알림을 받고 갱신되는 구조
- 즉, 1 → N(일대다) 의존성을 정의하는 패턴
- 흔히 Pub/Sub(발행/구독) 모델이라고도 부른다.
- 효과
- 상태 변화를 자동 전파
- 느슨한 결합(Loose Coupling) 확보
- 새로운 옵저버를 쉽게 추가/삭제할 수 있어 OCP(Open/Closed 원칙) 만족
- 뉴스나 유튜브 같은 것!
- 유튜버(Subject) ↔ 구독자(Observers)
- 유튜버가 영상 업로드 → 여러명의 구독자에게 영상 업로드 알림
- 구독자들은 -해당 채널을 구독함으로써 채널에 어떠한 변화(영상을 올리는 등)가 생기게 되면 바로 연락을 받아 탐지
- 반면 구독 안 한 시청자에게는 알림이 가지 않고, 구독 해지하면 마찬가지
2. 왜 필요한가?
- 변화(Change) 는 피할 수 없다
- 변화가 일어나면 동시에 여러 곳에 반영해야 할 때가 많다
- 날씨 데이터 변경 → 현재 날씨/통계/예보 화면이 자동 갱신
- 버튼 클릭 → 여러 이벤트 핸들러 실행
- 주문 생성 → 이메일 발송, 포인트 적립, 재고 차감
3. Observer Pattern 기본 구조
- Subject 인터페이스
- `registerObserver()`, `removeObserver()`, `notifyObservers()`
- → 옵저버 등록/제거/알림을 담당
- Observer 인터페이스
- `update()`
- → 상태 변경 시 호출되는 메서드
- ConcreteSubject
- 실제 데이터를 들고 있고, 상태가 변하면 `notifyObservers()` 실행
- ConcreteObserver
- `update()`를 구현해 변경된 상태를 반영

👉 핵심: 구현체가 아니라 인터페이스에 의존해서 의존성과 결합도를 낮춘다. (Loose Coupling)
느슨한 결합?
더보기
- 느슨한 결합(Loose Coupling): 서로 필요한 최소한의 정보만 알아도 상호작용이 가능한 것.
- Subject는 Observer에 대해 “Observer 인터페이스를 구현했다” 정도만 알면 됨
- 그 객체가 구체적으로 뭘 하는지는 X
- 언제든 새로운 Observer 추가 가능
- 언제든 Observer 제거 가능
- Subject는 수정할 필요 없음
- Observer와 Subject를 독립적으로 재사용 가능
- 👉 디자인 원칙: “상호작용하는 객체들 사이에는 느슨한 결합을 유지하라.”
- 변경(유연), 재사용성, 유지보수
설계 핵심 요약:
1. 옵저버는 등록(register/subscribe) 해야 알림을 받는다.
2. 주제는 얼마나 많은 옵저버가 있는지 몰라도 된다.(느슨한 결합)
3. 상태가 바뀌면, 주제는 '일괄 통지'만 한다. 누가 받는지, 받는 쪽에서 무엇을 하는지 관여하지 않는다.
4. 옵저버는 통지받을 때 필요한 데이터를 받거나(푸시), 주제에게 읽어오거나(풀) 하도록 게약(인터페이스)만 맞춘다.
5. 미가입 객체는 영향 없음 → 확장/교체/선택적 참여가 가능.
4. 패턴으로 문제 해결
1) 예시 → 기상 관측
- 상황: Weather-O-Rama 회사가 인터넷 기반 기상 관측 앱을 만든다.
- 요구사항
- 센서에서 온도/습도/기압을 측정
- 세 가지 화면 제공: 현재 상태, 통계, 예보
- 데이터가 바뀌면 화면도 실시간 업데이트
- 새 디스플레이를 쉽게 추가할 수 있어야 함
WeatherData - Subject
디스플레이들 - Observers
- 클래스 메서드 목록:
- `getTemperature()`, `getHumidity()`, `getPressure()` → 가장 최근 값을 돌려준다.
- `measurementsChanged()` → “측정값이 갱신될 때 호출되는” 콜백 메서드. 아직 비어 있음.
- 즉, 이 안에서 업데이트 메커니즘이 필요함.
2) 잘못된 접근(직접 호출)
public class WeatherData {
// instance variable declarations
public void measurementsChanged() {
float temp = getTemperature();
float humidity = getHumidity();
float pressure = getPressure();
currentConditionsDisplay.update(temp, humidity, pressure);
statisticsDisplay.update(temp, humidity, pressure);
forecastDisplay.update(temp, humidity, pressure);
}
// other WeatherData methods here
}
- “업데이트 시점(measurementsChanged)”에 각 화면의 update를 직접 호출
- 주제가 구체 클래스에 직접 의존
- 새 화면이 추가될 때마다 주제 코드를 수정해야 함 (OCP 위반)
- 런타임에 디스플레이를 추가/삭제할 수 없음
- 문제 요약
- 느슨한 결합 실패/확장성 부족, 재사용성 떨어짐
- 변화하는 축(옵저버 수·종류)을 캡슐화하지 못함
3) Observer Pattern!
- 계약(인터페이스) 분리
- `Subject`: 등록/제거/알림
- `Observer`: `update()`로 값을 받음 (알림 받음)
- `DisplayElement`: `display()` (화면에 표시하는 책임 분리)
- 구현
- WeatherData → Subject 구현체
- 각 디스플레이 → Observer + DisplayElement 구현체
- 새 디스플레이를 추가해도 WeatherData 수정 없음

더보기
- Subject 인터페이스
- `registerObserver()` / `removeObserver()` / `notifyObservers()`
- → 관찰자 관리 & 통지 책임 (구현 방식은 캡슐화)
- Observer 인터페이스
- `update()`
- → 주제가 바뀌면 알림 받고 반응
- DisplayElement 인터페이스
- `display()`
- → 화면 표시 책임 분리 (Observer와 별도 관심사)
- WeatherData (Subject 구현체)
- Subject 구현 + 온도/습도/기압 getter
- 값 변경 시 notifyObservers() 호출 → 브로드캐스트
- 디스플레이들 (Observers + DisplayElement)
- `CurrentConditionsDisplay`, `StatisticsDisplay`, `ForecastDisplay`
- → 업데이트 + 표시 동시에 담당
- ThirdPartyDisplay
- Observer/DisplayElement 구현만 하면 외부 개발자도 플러그인 추가 가능
- → OCP (개방–폐쇄 원칙) 실현
- 구조 핵심
- Subject/Observer = 알림 계약
- DisplayElement = 표시 계약
- WeatherData = Subject, 화면들 = Observer + DisplayElement
- 생성 시 자동 등록, 느슨한 결합 유지
알림 흐름

4) Push vs Pull 모델
- Push: `update(온도, 습도, 기압)` 등등 데이터 자체를 통지
- 장점: 단순함
- 단점: 전달 필드가 늘어나면 모든 옵저버를 수정해야 함
- Pull:`update()`는 “업데이트됨” 신호만 받고, 옵저버가 getter로 필요한 값 가져옴
- 장점: 인터페이스 안정성, 유연성
- 단점: 옵저버가 주제 참조를 알아야 함
| 구분 | Push | Pull |
| `update()` | 값들을 인자로 전달 | 신호만 받고, 값은 getter로 조회 |
| 장점 | 옵저버 구현이 단순, 호출 1번에 끝 | 인터페이스 안정(필드 추가에도 시그니처 고정), 옵저버별 필요한 값만 사용 |
| 단점 | 전달 필드가 늘면 모든 옵저버 수정 | 옵저버가 주제 참조 필요, 조회 타이밍 고려 |
상황 따라 선택
5. 구현 예시 (Push 방식)
1. 인터페이스 분리
// Subject: 옵저버 관리 + 알림
interface Subject {
void registerObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers();
}
// Observer(푸시): 알림과 함께 값 수신
interface Observer {
void update(float temperature, float humidity, float pressure);
}
// 화면: 출력
interface DisplayElement {
void display();
}
2. Subject 구현체 (`WeatherData`)
- 누가 구독 중인가?(리스트 가지고 있음)
- 값이 바뀌면 구독자들에게 알림!
import java.util.ArrayList;
import java.util.List;
class WeatherData implements Subject {
private final List<Observer> observers = new ArrayList<>();
private float temperature;
private float humidity;
private float pressure;
@Override
public void registerObserver(Observer o) {
if (o != null && !observers.contains(o)) observers.add(o);
}
@Override
public void removeObserver(Observer o) {
observers.remove(o);
}
@Override
public void notifyObservers() {
// Push: 값 자체를 밀어넣어 전달
for (Observer o : observers) {
o.update(temperature, humidity, pressure);
}
}
// 센서 값이 바뀔 때 호출되는 지점(훅)
public void measurementsChanged() {
notifyObservers();
}
// 테스트/실행용: 내부 상태를 바꾸고 통지
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
// (선택) 게터 — push에선 필수 아님, pull 전환 시 유용
public float getTemperature() { return temperature; }
public float getHumidity() { return humidity; }
public float getPressure() { return pressure; }
}
3. Observer 구현체 (Observers + DisplayElement)
- 생성자에서 Subject 구독
- `update()` : 알림 받기
- 알림 받으면 값 저장하고 출력함 (`display()`)
// -- 현재 상태
class CurrentConditionsDisplay implements Observer, DisplayElement {
private float temperature;
private float humidity;
private final Subject weatherData;
public CurrentConditionsDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this); // 생성과 동시에 등록(플러그인 느낌)
}
@Override
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
display();
}
@Override
public void display() {
System.out.println("Current: " + temperature + "F, " + humidity + "% humidity");
}
}
// -- 통계
class StatisticsDisplay implements Observer, DisplayElement {
private float sumTemp = 0f;
private int numReadings = 0;
private final Subject weatherData;
public StatisticsDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
@Override
public void update(float temperature, float humidity, float pressure) {
sumTemp += temperature;
numReadings++;
display();
}
@Override
public void display() {
float avg = (numReadings == 0) ? 0f : sumTemp / numReadings;
System.out.println("Stats: avg temp = " + avg);
}
}
// -- 예보
class ForecastDisplay implements Observer, DisplayElement {
private float currentPressure = 29.92f;
private float lastPressure;
private final Subject weatherData;
public ForecastDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
@Override
public void update(float temperature, float humidity, float pressure) {
lastPressure = currentPressure;
currentPressure = pressure;
display();
}
@Override
public void display() {
if (currentPressure > lastPressure) {
System.out.println("Forecast: Improving weather on the way!");
} else if (currentPressure == lastPressure) {
System.out.println("Forecast: More of the same");
} else {
System.out.println("Forecast: Watch out for cooler, rainy weather");
}
}
}
4. 실행
public class WeatherStation {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
new CurrentConditionsDisplay(weatherData);
new StatisticsDisplay(weatherData);
new ForecastDisplay(weatherData);
weatherData.setMeasurements(80, 65, 30.4f);
weatherData.setMeasurements(82, 70, 29.2f);
weatherData.setMeasurements(78, 90, 29.2f);
}
}
5. 확장 - HeatIndexDisplay 추가
- 만약 체감온도(`HeatIndexDisplay`)도 추가하라고 한다면? (공식 존재 가정)
- WeatherData(Subject)는 수정하지 않고, 화면(Observer + DisplayElement)만 수정해야 한다. → OCP
class HeatIndexDisplay implements Observer, DisplayElement {
private float heatIndex;
private final Subject weatherData;
// 구독 등록
public HeatIndexDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
// 업데이트 콜백 - push 방식 : Subject가 값(temperature, humidity, pressure)을 '밀어넣어' 전달
@Override
public void update(float t, float rh, float pressure) {
heatIndex = computeHeatIndex(t, rh);
display();
}
private float computeHeatIndex(float t, float rh) {
return (float)( // 공식 );
}
@Override
public void display() {
System.out.println("Heat index: " + heatIndex);
}
}
- Subject(WeatherData) 수정 없음. → 플러그인처럼 추가/제거.
- 메시징/스트림(Rx, Kafka 소비자)은 대규모 옵저버로 볼 수 있음
6. Pull 방식
- JDK 내장(Observable) 버전 변형
1. Subject
import java.util.Observable;
public class WeatherData extends Observable {
private float temperature;
private float humidity;
private float pressure;
public void setMeasurements(float t, float h, float p) {
this.temperature = t;
this.humidity = h;
this.pressure = p;
measurementsChanged();
}
// 알림 보내기
public void measurementsChanged() {
setChanged(); // 반드시 먼저 호출
notifyObservers(); // 인자 없음 → PULL 모델
}
// PULL용 getter
public float getTemperature() { return temperature; }
public float getHumidity() { return humidity; }
public float getPressure() { return pressure; }
}
2. HeatIndexDisplay (Observer)
import java.util.Observable;
import java.util.Observer;
public class HeatIndexDisplay implements Observer, DisplayElement {
private float heatIndex;
private final Observable observable; // Observable 참조를 보관(해지/재등록 대비)
public HeatIndexDisplay(Observable observable) {
this.observable = observable;
observable.addObserver(this); //생성 시 등록
}
@Override
public void update(Observable o, Object arg) {
if (o instanceof WeatherData) { // Subject 확인 후 캐스팅
WeatherData wd = (WeatherData) o;
float t = wd.getTemperature(); // Pull: getter로 끌어옴
float rh = wd.getHumidity();
heatIndex = computeHeatIndex(t, rh);
display();
}
}
private float computeHeatIndex(float t, float rh) {
return (float)(
// 공식
);
}
@Override
public void display() {
System.out.printf("Heat index: %.2f%n", heatIndex);
}
// (선택) 해지 지원
public void unsubscribe() {
observable.deleteObserver(this);
}
}
- `java.util.Observable`은 인터페이스가 아니라 클래스 → 상속해야 함.
- 알림은 2단계: `setChanged()` → `notifyObservers()`
- 알림 순서 비보장→ 순서 의존 로직 금지.
JDK로 Push 사용하기?
더보기
Subject:
setChanged();
notifyObservers(new Measurements(temp, humidity, pressure)); // Push
Observer:
public void update(Observable o, Object arg) {
if (o instanceof WeatherData && arg instanceof Measurements m) {
heatIndex = computeHeatIndex(m.temp(), m.humidity());
display();
}
}
핵심 규칙은 동일: setChanged() → notifyObservers(…), 그리고 순서 의존 금지.
7. 확인해야 할 것
- [JDK 버전] `setChanged()` 호출 누락 → `notifyObservers()`가 무효
- 구현체 의존: `currentConditionsDisplay.update(...)`처럼 구체 클래스에 직접 의존하지 말 것 → 인터페이스(Observer) 로.
- 알림 순서 가정: “항상 A가 먼저, 그다음 B” 같은 가정 금지.
- 등록/해지 누락: 생성 시 `registerObserver(this)`(또는 `addObserver`) 잊지 말고, 필요 시 `removeObserver` 구현/호출.
- 불필요한 결합: 표시(UI) 책임과 관찰(업데이트) 책임은 분리(`DisplayElement`)가 깔끔.
8. 앞 Strategy 패턴과 비교
- Strategy

- Observer

| 항목 | Strategy | Observer |
| 목적 | 동작(알고리즘) 교체 | 상태 변화 알림(1→N) |
| 결합도 | 컨텍스트↔전략 (구성) | 주제↔옵저버 (콜백) |
| 확장 | 새 전략 추가 | 새 옵저버 추가 |
9. 구현 예시 (Java)
시나리오
- 주식 가격 알림 시스템
- 특정 주식의 가격이 변하면 → 모든 구독자(앱, SMS, 이메일)에 알림.
- 주제(Subject): Stock
- 옵저버(Observer): Display/알림 채널
코드
import java.util.ArrayList;
import java.util.List;
// Subject 인터페이스
interface Subject {
void registerObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers();
}
// Observer 인터페이스
interface Observer {
void update(float price);
}
// 주식 클래스 (Subject)
class Stock implements Subject {
private List<Observer> observers = new ArrayList<>();
private float price;
@Override
public void registerObserver(Observer o) {
observers.add(o);
}
@Override
public void removeObserver(Observer o) {
observers.remove(o);
}
@Override
public void notifyObservers() {
for (Observer o : observers) {
o.update(price);
}
}
public void setPrice(float price) {
this.price = price;
notifyObservers(); // 가격 변경 시 자동 알림
}
}
// Observer: 모바일 앱 알림
class MobileApp implements Observer {
private String name;
public MobileApp(String name) {
this.name = name;
}
@Override
public void update(float price) {
System.out.println(name + " 앱: 주식 가격 업데이트 - " + price + "원");
}
}
// Observer: 이메일 알림
class EmailAlert implements Observer {
private String email;
public EmailAlert(String email) {
this.email = email;
}
@Override
public void update(float price) {
System.out.println("이메일(" + email + "): 가격 알림 - " + price + "원");
}
}
// 실행 예시
public class Main {
public static void main(String[] args) {
Stock samsung = new Stock();
MobileApp app1 = new MobileApp("1번 증권");
MobileApp app2 = new MobileApp("2번 증권");
EmailAlert emailAlert = new EmailAlert("user@email.com");
// 옵저버 등록
samsung.registerObserver(app1);
samsung.registerObserver(app2);
samsung.registerObserver(emailAlert);
// 가격 변경 → 자동으로 모든 옵저버에게 알림
samsung.setPrice(55000);
samsung.setPrice(56000);
}
}
- pull로 바꾼다면?
// Observer: 주제 참조만 받음
interface Observer { void update(Subject s); }
// Stock: 값은 getter로 노출, 알림은 주체 자신을 전달
class Stock implements Subject {
...
public float getPrice() { return price; }
public void notifyObservers() {
for (Observer o : observers) o.update(this);
}
}
// 옵저버 쪽: 필요할 때 주제에서 끌어오기
class MobileApp implements Observer {
public void update(Subject s) {
float price = ((Stock) s).getPrice();
System.out.println(name + " 가격 알림 - " + price + "원");
}
}
10. 마무리
- 캡슐화
- 상태 또는 Observer 수·종류 → 변하는 것 → Subject 내부로 캡슐화
- Subject 코드를 건드리지 않고도 옵저버를 자유롭게 추가/제거
- 인터페이스 프로그래밍
- Subject↔Observer는 인터페이스로 통신(느슨한 결합)
- Subject는 “Observer 인터페이스를 구현한 무언가”만 알면 됨
- Observer는 “Subject 인터페이스”에 등록/알림만 알면 됨.
- 상속보다는 객체 조합
- 옵저버 연결은 상속이 아닌 등록/해지로 런타임 조합.
참고
⌜Head First Design Patterns⌟ Eric Freeman 외, O’Reilly Media, 2004
[Chapter ] (p.~)
* 책을 참고해 필요한 내용을 정리한 것이므로, 책의 내용과 다를 수 있습니다.
300x250
반응형
GitHub 댓글