title:
style: nestedList # TOC style (nestedList|nestedOrderedList|inlineFirstLevel)
minLevel: 0 # Include headings from the specified level
maxLevel: 0 # Include headings up to the specified level
includeLinks: true # Make headings clickable
hideWhenEmpty: false # Hide TOC if no headings are found
debugInConsole: false # Print debug info in Obsidian console옵저버 패턴(Observer Pattern) 완벽 가이드
옵저버 패턴 개념 설명
기본 개념
옵저버 패턴은 객체 간의 일대다(one-to-many) 의존성을 정의하는 행동 디자인 패턴이다. 한 객체(Subject)의 상태가 변경되면 그 객체에 의존하는 모든 객체(Observer)들이 자동으로 알림을 받고 갱신된다.
실생활 비유로는 뉴스 구독 서비스와 유사하다. 신문사(Subject)가 새로운 뉴스를 발행하면 모든 구독자(Observer)는 자동으로 새 뉴스를 받게 된다. 구독자는 언제든지 구독을 시작하거나 취소할 수 있다.
핵심 구성 요소
-
Subject(주체): 관찰 대상이 되는 객체
- Observer를 등록/제거하는 메서드 제공
- Observer에게 상태 변경을 알리는 메서드 포함
-
Observer(관찰자): Subject의 변화를 감지하는 객체
- Subject로부터 상태 변경 알림을 받는 인터페이스 구현
- 알림 수신 시 특정 동작 수행
-
ConcreteSubject: Subject 인터페이스 구현체
- 실제 상태를 가지며 변경 시 Observer에게 알림
-
ConcreteObserver: Observer 인터페이스 구현체
- Subject의 상태 변경에 대응하는 특정 동작 정의
옵저버 패턴 구조
classDiagram class Subject { +attach(observer) +detach(observer) +notify() } class Observer { +update() } class ConcreteSubject { -state +getState() +setState() } class ConcreteObserver { -observerState +update() } Subject <|-- ConcreteSubject Observer <|-- ConcreteObserver Subject "1" --> "*" Observer
기본 동작 방식
옵저버 패턴 작동 흐름
- Subject는 Observer 목록을 유지관리한다
- Observer가 Subject를 구독하면 목록에 추가된다
- Observer가 구독 취소하면 목록에서 제거된다
- Subject의 상태가 변경되면 모든 Observer에게 알림을 보낸다
- 각 Observer는 알림을 받아 적절한 동작을 수행한다
sequenceDiagram participant Client participant Subject participant Observer1 participant Observer2 Client->>Subject: 생성 Client->>Observer1: 생성 Client->>Observer2: 생성 Client->>Subject: attach(observer1) Subject->>Subject: observers.add(observer1) Client->>Subject: attach(observer2) Subject->>Subject: observers.add(observer2) Client->>Subject: setState(newState) Subject->>Subject: state = newState Subject->>Subject: notify() Subject->>Observer1: update() Observer1->>Subject: getState() Subject-->>Observer1: state Subject->>Observer2: update() Observer2->>Subject: getState() Subject-->>Observer2: state Client->>Subject: detach(observer1) Subject->>Subject: observers.remove(observer1) Client->>Subject: setState(newState2) Subject->>Subject: state = newState2 Subject->>Subject: notify() Subject->>Observer2: update() Observer2->>Subject: getState() Subject-->>Observer2: state
실제 사용 예시
Python 구현 예시
from abc import ABC, abstractmethod
from typing import List
class Observer(ABC):
"""관찰자 인터페이스"""
@abstractmethod
def update(self, temperature: float, humidity: float, pressure: float) -> None:
"""상태 업데이트 시 호출되는 메서드"""
pass
class Subject(ABC):
"""주체 인터페이스"""
@abstractmethod
def register_observer(self, observer: Observer) -> None:
"""관찰자 등록"""
pass
@abstractmethod
def remove_observer(self, observer: Observer) -> None:
"""관찰자 제거"""
pass
@abstractmethod
def notify_observers(self) -> None:
"""모든 관찰자에게 알림"""
pass
class WeatherData(Subject):
"""구체적인 주체 구현: 날씨 데이터"""
def __init__(self):
self._observers: List[Observer] = []
self._temperature: float = 0
self._humidity: float = 0
self._pressure: float = 0
def register_observer(self, observer: Observer) -> None:
"""관찰자 등록"""
if observer not in self._observers:
self._observers.append(observer)
def remove_observer(self, observer: Observer) -> None:
"""관찰자 제거"""
if observer in self._observers:
self._observers.remove(observer)
def notify_observers(self) -> None:
"""모든 관찰자에게 알림"""
for observer in self._observers:
observer.update(self._temperature, self._humidity, self._pressure)
def measurements_changed(self) -> None:
"""측정값 변경 시 호출"""
self.notify_observers()
def set_measurements(self, temperature: float, humidity: float, pressure: float) -> None:
"""새로운 측정값 설정"""
self._temperature = temperature
self._humidity = humidity
self._pressure = pressure
self.measurements_changed()
class CurrentConditionsDisplay(Observer):
"""구체적인 관찰자 구현: 현재 날씨 상태 디스플레이"""
def __init__(self, weather_data: WeatherData):
self._temperature: float = 0
self._humidity: float = 0
self._weather_data = weather_data
weather_data.register_observer(self)
def update(self, temperature: float, humidity: float, pressure: float) -> None:
"""상태 업데이트"""
self._temperature = temperature
self._humidity = humidity
self.display()
def display(self) -> None:
"""화면에 정보 표시"""
print(f"현재 상태: 온도 {self._temperature}°C, 습도 {self._humidity}%")
class StatisticsDisplay(Observer):
"""구체적인 관찰자 구현: 통계 디스플레이"""
def __init__(self, weather_data: WeatherData):
self._temperature_sum: float = 0
self._reading_count: int = 0
self._max_temp: float = float('-inf')
self._min_temp: float = float('inf')
self._weather_data = weather_data
weather_data.register_observer(self)
def update(self, temperature: float, humidity: float, pressure: float) -> None:
"""상태 업데이트"""
self._temperature_sum += temperature
self._reading_count += 1
if temperature > self._max_temp:
self._max_temp = temperature
if temperature < self._min_temp:
self._min_temp = temperature
self.display()
def display(self) -> None:
"""화면에 정보 표시"""
avg_temp = self._temperature_sum / self._reading_count if self._reading_count > 0 else 0
print(f"날씨 통계: 평균/최고/최저 온도 = {avg_temp:.1f}/{self._max_temp:.1f}/{self._min_temp:.1f}")
# 사용 예시
def main():
weather_data = WeatherData()
# 디스플레이 생성 (관찰자 등록)
current_display = CurrentConditionsDisplay(weather_data)
statistics_display = StatisticsDisplay(weather_data)
# 날씨 데이터 업데이트
print("첫 번째 측정값 설정:")
weather_data.set_measurements(25.2, 65.0, 1013.1)
print("\n두 번째 측정값 설정:")
weather_data.set_measurements(26.5, 70.0, 1010.5)
# 관찰자 제거
weather_data.remove_observer(current_display)
print("\n세 번째 측정값 설정 (현재 상태 디스플레이 제거 후):")
weather_data.set_measurements(24.8, 80.0, 1012.0)
if __name__ == "__main__":
main()실행 결과:
첫 번째 측정값 설정:
현재 상태: 온도 25.2°C, 습도 65.0%
날씨 통계: 평균/최고/최저 온도 = 25.2/25.2/25.2
두 번째 측정값 설정:
현재 상태: 온도 26.5°C, 습도 70.0%
날씨 통계: 평균/최고/최저 온도 = 25.9/26.5/25.2
세 번째 측정값 설정 (현재 상태 디스플레이 제거 후):
날씨 통계: 평균/최고/최저 온도 = 25.5/26.5/24.8
JavaScript 구현 예시
// Observer 패턴 - JavaScript 구현
// Subject (주체) 클래스
class NewsPublisher {
constructor() {
this.subscribers = [];
this.news = '';
}
// Observer 등록
subscribe(observer) {
if (!this.subscribers.includes(observer)) {
this.subscribers.push(observer);
console.log(`${observer.name}님이 뉴스 구독을 시작했습니다.`);
}
}
// Observer 제거
unsubscribe(observer) {
const index = this.subscribers.indexOf(observer);
if (index !== -1) {
this.subscribers.splice(index, 1);
console.log(`${observer.name}님이 뉴스 구독을 취소했습니다.`);
}
}
// 모든 Observer에게 알림
notify() {
this.subscribers.forEach(subscriber => {
subscriber.update(this.news);
});
}
// 새 뉴스 발행 (상태 변경)
publishNews(news) {
this.news = news;
console.log(`\n새로운 뉴스 발행: "${this.news}"`);
this.notify();
}
}
// Observer (관찰자) 클래스
class Subscriber {
constructor(name) {
this.name = name;
}
// 상태 업데이트 시 호출되는 메서드
update(news) {
console.log(`${this.name}: 새 뉴스를 받았습니다 - "${news}"`);
}
}
// 특별한 Observer 타입 - 푸시 알림 구독자
class PushSubscriber extends Subscriber {
update(news) {
console.log(`🔔 ${this.name}에게 푸시 알림: "${news}"`);
}
}
// 특별한 Observer 타입 - 프리미엄 구독자
class PremiumSubscriber extends Subscriber {
update(news) {
console.log(`✨ ${this.name}(프리미엄): "${news}" (추가 분석 포함)`);
}
}
// 사용 예시
function runExample() {
// Subject 생성
const techNews = new NewsPublisher();
// Observer 생성
const john = new Subscriber('John');
const jane = new PushSubscriber('Jane');
const sam = new PremiumSubscriber('Sam');
// Observer 등록
techNews.subscribe(john);
techNews.subscribe(jane);
techNews.subscribe(sam);
// 뉴스 발행 (상태 변경)
techNews.publishNews('인공지능 기술 발전으로 새로운 산업 혁명 시작');
// Observer 제거
techNews.unsubscribe(john);
// 다시 뉴스 발행
techNews.publishNews('양자 컴퓨팅 돌파구 발견');
}
// 예시 실행
runExample();실행 결과:
John님이 뉴스 구독을 시작했습니다.
Jane님이 뉴스 구독을 시작했습니다.
Sam님이 뉴스 구독을 시작했습니다.
새로운 뉴스 발행: "인공지능 기술 발전으로 새로운 산업 혁명 시작"
John: 새 뉴스를 받았습니다 - "인공지능 기술 발전으로 새로운 산업 혁명 시작"
🔔 Jane에게 푸시 알림: "인공지능 기술 발전으로 새로운 산업 혁명 시작"
✨ Sam(프리미엄): "인공지능 기술 발전으로 새로운 산업 혁명 시작" (추가 분석 포함)
John님이 뉴스 구독을 취소했습니다.
새로운 뉴스 발행: "양자 컴퓨팅 돌파구 발견"
🔔 Jane에게 푸시 알림: "양자 컴퓨팅 돌파구 발견"
✨ Sam(프리미엄): "양자 컴퓨팅 돌파구 발견" (추가 분석 포함)
PHP 구현 예시
<?php
// Observer 인터페이스
interface Observer {
public function update(string $message): void;
}
// Subject 인터페이스
interface Subject {
public function attach(Observer $observer): void;
public function detach(Observer $observer): void;
public function notify(): void;
}
// 구체적인 Subject: 이벤트 관리자
class EventManager implements Subject {
private array $observers = [];
private string $eventName = '';
private string $eventDescription = '';
public function attach(Observer $observer): void {
$observerId = spl_object_hash($observer);
if (!isset($this->observers[$observerId])) {
$this->observers[$observerId] = $observer;
echo get_class($observer) . "가 이벤트 관리자에 등록되었습니다.\n";
}
}
public function detach(Observer $observer): void {
$observerId = spl_object_hash($observer);
if (isset($this->observers[$observerId])) {
unset($this->observers[$observerId]);
echo get_class($observer) . "가 이벤트 관리자에서 제거되었습니다.\n";
}
}
public function notify(): void {
foreach ($this->observers as $observer) {
$observer->update("이벤트 발생: {$this->eventName} - {$this->eventDescription}");
}
}
public function createEvent(string $name, string $description): void {
$this->eventName = $name;
$this->eventDescription = $description;
echo "\n새 이벤트 생성: '{$name}'\n";
$this->notify();
}
}
// 구체적인 Observer: 이메일 알림
class EmailNotifier implements Observer {
private string $email;
public function __construct(string $email) {
$this->email = $email;
}
public function update(string $message): void {
echo "📧 이메일 ({$this->email})로 알림 전송: {$message}\n";
}
}
// 구체적인 Observer: SMS 알림
class SmsNotifier implements Observer {
private string $phoneNumber;
public function __construct(string $phoneNumber) {
$this->phoneNumber = $phoneNumber;
}
public function update(string $message): void {
echo "📱 SMS ({$this->phoneNumber})로 알림 전송: {$message}\n";
}
}
// 구체적인 Observer: 앱 내 알림
class AppNotifier implements Observer {
private string $username;
public function __construct(string $username) {
$this->username = $username;
}
public function update(string $message): void {
echo "🔔 앱 내 알림 (사용자: {$this->username}): {$message}\n";
}
}
// 사용 예시
function runDemo() {
// Subject 생성
$eventManager = new EventManager();
// Observer 생성
$emailNotifier = new EmailNotifier("user@example.com");
$smsNotifier = new SmsNotifier("010-1234-5678");
$appNotifier = new AppNotifier("john_doe");
// Observer 등록
$eventManager->attach($emailNotifier);
$eventManager->attach($smsNotifier);
$eventManager->attach($appNotifier);
// 이벤트 생성 (상태 변경)
$eventManager->createEvent("신제품 출시", "새로운 제품이 출시되었습니다.");
// Observer 제거
$eventManager->detach($smsNotifier);
// 다른 이벤트 생성
$eventManager->createEvent("특별 할인", "오늘만 30% 할인 이벤트를 진행합니다.");
}
// 실행
runDemo();
?>실행 결과:
EmailNotifier가 이벤트 관리자에 등록되었습니다.
SmsNotifier가 이벤트 관리자에 등록되었습니다.
AppNotifier가 이벤트 관리자에 등록되었습니다.
새 이벤트 생성: '신제품 출시'
📧 이메일 (user@example.com)로 알림 전송: 이벤트 발생: 신제품 출시 - 새로운 제품이 출시되었습니다.
📱 SMS (010-1234-5678)로 알림 전송: 이벤트 발생: 신제품 출시 - 새로운 제품이 출시되었습니다.
🔔 앱 내 알림 (사용자: john_doe): 이벤트 발생: 신제품 출시 - 새로운 제품이 출시되었습니다.
SmsNotifier가 이벤트 관리자에서 제거되었습니다.
새 이벤트 생성: '특별 할인'
📧 이메일 (user@example.com)로 알림 전송: 이벤트 발생: 특별 할인 - 오늘만 30% 할인 이벤트를 진행합니다.
🔔 앱 내 알림 (사용자: john_doe): 이벤트 발생: 특별 할인 - 오늘만 30% 할인 이벤트를 진행합니다.
고급 활용법
이벤트 시스템 구현
옵저버 패턴은 이벤트 기반 프로그래밍의 핵심이다. 이벤트 발생 시 등록된 리스너(observer)에게 알림을 전달하는 방식으로 동작한다.
// JavaScript 이벤트 시스템 구현 예시
class EventEmitter {
constructor() {
this.events = {};
}
// 이벤트 리스너 등록
on(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(listener);
return this;
}
// 이벤트 리스너 제거
off(event, listener) {
if (!this.events[event]) return this;
this.events[event] = this.events[event].filter(l => l !== listener);
return this;
}
// 이벤트 발생
emit(event, ...args) {
if (!this.events[event]) return this;
this.events[event].forEach(listener => {
listener.apply(this, args);
});
return this;
}
// 한 번만 실행되는 이벤트 리스너 등록
once(event, listener) {
const onceWrapper = (...args) => {
listener.apply(this, args);
this.off(event, onceWrapper);
};
return this.on(event, onceWrapper);
}
}
// 사용 예시
const userEvents = new EventEmitter();
function userLoginHandler(user) {
console.log(`${user}님이 로그인했습니다.`);
}
function userActivityHandler(user, activity) {
console.log(`${user}님이 ${activity} 활동을 수행했습니다.`);
}
// 이벤트 리스너 등록
userEvents.on('login', userLoginHandler);
userEvents.on('activity', userActivityHandler);
// 이벤트 발생
userEvents.emit('login', '김철수');
userEvents.emit('activity', '김철수', '댓글 작성');
// 이벤트 리스너 제거
userEvents.off('login', userLoginHandler);
// 이벤트 리스너 제거 후 발생
userEvents.emit('login', '박영희'); // 출력 없음
userEvents.emit('activity', '박영희', '글 작성');푸시 기반 vs. 풀 기반 모델
-
푸시 기반 모델
- Subject가 Observer에게 상세 데이터를 전달
- 장점: Observer가 추가 요청 없이 필요한 데이터를 한 번에 받음
- 단점: Observer가 필요하지 않은 데이터까지 전달받을 수 있음
-
풀 기반 모델
- Subject는 상태 변경 알림만 전달
- Observer가 필요한 데이터를 Subject에게 요청
- 장점: Observer가 필요한 데이터만 선택적으로 가져올 수 있음
- 단점: 데이터 조회를 위한 추가 호출 필요
# 풀 기반 모델 예시
class WeatherData:
def __init__(self):
self._observers = []
self._temperature = 0
self._humidity = 0
self._pressure = 0
def register_observer(self, observer):
if observer not in self._observers:
self._observers.append(observer)
def remove_observer(self, observer):
if observer in self._observers:
self._observers.remove(observer)
def notify_observers(self):
# 데이터를 푸시하지 않고 변경 사실만 알림
for observer in self._observers:
observer.update(self) # Subject 자체를 전달
# 상태 접근자 메서드들
def get_temperature(self):
return self._temperature
def get_humidity(self):
return self._humidity
def get_pressure(self):
return self._pressure
def set_measurements(self, temperature, humidity, pressure):
self._temperature = temperature
self._humidity = humidity
self._pressure = pressure
self.notify_observers()
class TemperatureDisplay:
def update(self, weather_data):
# 필요한 데이터만 가져옴 (풀)
temperature = weather_data.get_temperature()
print(f"현재 온도: {temperature}°C")
class HumidityDisplay:
def update(self, weather_data):
# 필요한 데이터만 가져옴 (풀)
humidity = weather_data.get_humidity()
print(f"현재 습도: {humidity}%")비동기 Observer 패턴
현대 웹 애플리케이션에서는 비동기 이벤트 처리가 중요하다. Promise, async/await과 결합한 Observer 패턴은 매우 유용하다.
// 비동기 Observer 패턴 예시
class AsyncNewsPublisher {
constructor() {
this.subscribers = [];
this.news = '';
}
subscribe(observer) {
this.subscribers.push(observer);
return {
unsubscribe: () => {
this.subscribers = this.subscribers.filter(obs => obs !== observer);
}
};
}
async publishNews(news) {
this.news = news;
console.log(`뉴스 발행 중: "${news}"`);
// 모든 Observer에게 알림 (비동기 처리)
const notificationPromises = this.subscribers.map(subscriber =>
Promise.resolve().then(() => subscriber.update(this.news))
);
// 모든 알림이 완료될 때까지 기다림
await Promise.all(notificationPromises);
console.log('모든 구독자에게 알림 완료');
}
}
// 비동기 Observer 예시
class AsyncSubscriber {
constructor(name, delay) {
this.name = name;
this.delay = delay; // 의도적 지연 (ms)
}
async update(news) {
// 비동기 처리 시뮬레이션
await new Promise(resolve => setTimeout(resolve, this.delay));
console.log(`${this.name} (${this.delay}ms 지연): 뉴스 수신 - "${news}"`);
return true;
}
}
// 사용 예시
async function runAsyncExample() {
const publisher = new AsyncNewsPublisher();
// 다양한 지연 시간을 가진 구독자
const subscriber1 = new AsyncSubscriber('빠른 구독자', 100);
const subscriber2 = new AsyncSubscriber('일반 구독자', 500);
const subscriber3 = new AsyncSubscriber('느린 구독자', 1000);
// 구독
const subscription1 = publisher.subscribe(subscriber1);
const subscription2 = publisher.subscribe(subscriber2);
const subscription3 = publisher.subscribe(subscriber3);
// 뉴스 발행 (비동기)
await publisher.publishNews('중요한 속보입니다!');
// 첫 번째 구독자 구독 취소
subscription1.unsubscribe();
// 다른 뉴스 발행
await publisher.publishNews('추가 정보가 있습니다.');
}
// 실행
runAsyncExample();주의사항
성능 고려사항
- Observer 관리: Observer가 많을 경우 메모리 사용량이 증가할 수 있다
- 순환 참조: Subject와 Observer 간 순환 참조 발생 가능성 주의
- 알림 순서: Observer 알림 순서에 의존하지 않도록 설계해야 한다
- 지연 알림: 많은 상태 변경 시 알림 지연(batch notification) 고려
자주 발생하는 문제점
- Memory Leak
- Observer 제거를 잊는 경우 메모리 누수 발생
- 해결: 약한 참조(weak reference) 사용, 명시적 구독 취소 메커니즘 구현
// 약한 참조를 활용한 Observer 패턴 (JavaScript)
class WeakPublisher {
constructor() {
// WeakMap을 사용하여 Observer 관리
this.observers = new WeakMap();
}
subscribe(observer, callback) {