이 실습에서는 Observer 패턴을 활용하여 주식 시세 모니터링, 센서 알림 시스템 등 이벤트 주도 아키텍처를 구현합니다.
실습 목표
- 주식 시세 모니터링 시스템 구현
- 온도 센서 알림 시스템 구현
- 성능 최적화 실습
과제 1: 주식 시세 모니터링
“한 객체의 상태가 변했을 때, 그 객체에 의존하는 다른 객체들에게 자동으로 알려주고 업데이트되도록 하는 일대다 의존성을 정의한다.” — GoF, 《Design Patterns》(1994), Observer 패턴 Intent
이 과제는 Observer 패턴의 가장 단순한 형태(Subject/Observer 인터페이스 분리)를 주식 시세라는 익숙한 도메인에 적용해보는 것이 목적이다. 화면 표시, 임계값 알림, 로그 기록처럼 성격이 다른 구독자들이 동일한 가격 변경 이벤트에 반응해야 할 때, Stock이 구독자의 구체 타입을 몰라도 통지할 수 있어야 한다는 점이 핵심이다.
기본 구조
아래는 Stock의 attach/detach/notifyObservers와 StockLogger 옵저버까지 포함한 완성 참조 구현이다. StockDisplay, StockAlert는 이 구조를 참고해 직접 구현한다.
| |
구현 과제
- StockDisplay, StockAlert 옵저버 구현 (위 StockLogger를 참고)
- 여러 주식 동시 모니터링 기능
과제 2: 온도 센서 알림
이 과제는 단순 통지가 아니라 조건부 통지(임계값을 넘었을 때만 반응)를 Observer 구조 위에 얹는 감각을 익히는 것이 목적이다. TemperatureSensor는 값이 바뀌었다는 사실만 알리고, 임계값 판단과 알림 채널 선택은 각 Observer가 책임진다는 역할 분리에 주목한다. attach/detach/notifyObservers는 과제 1의 Stock과 동일한 방식으로 이미 완성해 두었으니, 이번 과제에서는 그 위에 얹을 조건부 로직(임계값, 채널, 빈도 제한)에 집중한다.
기본 구조
| |
구현 과제
- 임계값 기반 알림 시스템 —
onTemperatureChanged내부에서 임계값을 넘었을 때만 실제 알림을 발생시키는 Observer 구현 - 다양한 알림 채널 (이메일, SMS, 로그)
- 알림 빈도 제한 기능
과제 3: 성능 최적화
이 과제는 Observer 패턴을 실서비스에 적용할 때 마주치는 두 가지 문제 — 등록 해제를 잊어 발생하는 메모리 누수, 동기 통지로 인한 호출 스레드 블로킹 — 을 완화하는 기법을 다룬다.
WeakReference를 언제 쓸 것인가: WeakReference는 detach() 호출을 잊어도 GC가 알아서 정리해준다는 장점이 있지만, 정리 시점이 GC 타이밍에 좌우되어 예측 불가능하고 notifyObservers()마다 죽은 참조를 순회하며 걸러내는 오버헤드가 붙는다. Subject를 attach한 코드가 자신의 생명주기를 명확히 통제할 수 있다면(예: 화면이 닫힐 때 확실히 detach를 호출할 수 있는 UI 컴포넌트) 명시적 해제가 더 예측 가능하고 저렴하다. 반대로 Subject가 전역 싱글턴이나 장수명 캐시처럼 Observer보다 훨씬 오래 살아남고 호출자가 detach를 안정적으로 보장하기 어려운 구조라면, WeakReference로 누수를 방지하는 편이 안전하다.
| 선택 기준 | WeakReference | 명시적 해제(detach) |
|---|---|---|
| Observer 생명주기 통제 | 호출자가 통제하기 어려움 | 호출자가 명확히 통제 가능 |
| Subject 수명 | Observer보다 훨씬 김(싱글턴, 캐시) | Observer와 비슷하거나 짧음 |
| 정리 시점 | GC 타이밍에 의존, 예측 불가 | detach 호출 즉시, 예측 가능 |
| 런타임 오버헤드 | notifyObservers마다 죽은 참조 스캔 | 없음 |
| 실수 시 위험 | 없음(자동 회수) | detach 누락 시 누수 |
| 적합한 상황 | 전역 캐시, 이벤트 버스 | UI 컴포넌트, 명확한 스코프 객체 |
WeakReference Observer
이 예시는 별도의 Observer 타입을 새로 만들지 않고, 과제 1에서 정의한 StockSubject/StockObserver를 그대로 구현한다. update의 시그니처가 update(String symbol, double price, double change)이므로, Stock처럼 symbol/price/change 필드를 두고 통지 시 이 값을 넘긴다.
| |
비동기 Observer
통지 자체는 즉시 반환하고, 실제 처리는 별도 스레드로 넘겨 호출 스레드(대개 Subject가 setPrice 등을 호출한 스레드)를 블로킹하지 않는다. processUpdate는 여기서 실제로 무거운 작업(가격 이력 집계, 알림 발송 등)을 수행하는 자리이며, 비동기 실행 중 발생한 예외가 조용히 삼켜지지 않도록 명시적으로 잡아 로깅해야 한다.
| |
완성도 체크리스트
아래 체크리스트는 과제 1~3에서 다룬 결함을 실제로 재현해 검증했는지 확인하는 용도다. 예를 들어 “메모리 누수 시나리오 테스트” 항목은 WeakReferenceStock 없이 순수 Stock에 10,000개의 Observer를 attach만 하고 detach하지 않은 채 힙 덤프를 떠 보면, 과제 3에서 설명한 강한 참조로 인한 누수가 실제로 관찰되는지 확인하라는 뜻이다. 체크박스에 표시하기 전에 해당 시나리오를 코드로 직접 재현해 보는 것을 권장한다.
기본 구현
- Subject/Observer 인터페이스 구현 —
Stock이 구체 Observer 타입을 몰라도StockObserver인터페이스만으로 통지할 수 있는지 확인 - 다양한 Observer 구현체 작성 — Display/Alert/Logger처럼 반응 방식이 다른 Observer가 동일한 인터페이스로 등록되는지 확인
- 동적 Observer 추가/제거 기능 — 실행 중에 attach/detach를 호출해도 다른 Observer의 통지가 깨지지 않는지 확인
- 예외 처리 (Observer 실패 시) — 하나의 Observer에서 예외가 발생해도 나머지 Observer가 정상 통지받는지 확인
고급 기능
- WeakReference 기반 메모리 누수 방지 — detach를 호출하지 않은 Observer가 GC 이후 목록에서 자동으로 사라지는지 확인
- 비동기 알림 처리 — 통지가 호출 스레드를 블로킹하지 않고 별도 스레드에서 처리되는지 확인
- 알림 필터링 및 우선순위 — 조건에 맞지 않는 이벤트는 걸러지고, 우선순위가 높은 Observer가 먼저 통지받는지 확인
- 성능 모니터링 및 최적화 — Observer 수 증가에 따른 통지 시간을 실측했는지 확인
테스트
- 다수 Observer 성능 테스트 — 수천 개 Observer 등록 시에도 통지 시간이 선형적으로 증가하는지 확인
- 메모리 누수 시나리오 테스트 — detach 없이 Observer를 반복 생성해도 힙 사용량이 계속 늘지 않는지 확인
- 동시성 테스트 — 여러 스레드가 동시에 attach/detach/notify를 호출해도 예외나 데이터 손상이 없는지 확인
추가 도전 과제
- EventBus 패턴으로 확장
- Reactive Streams 연계
- 분산 Observer 시스템
- 패턴 조합 (Observer + Strategy + Command)
실무 적용 예시
MVC 아키텍처와 Observer
Model이 Subject, View가 Observer를 맡는 이유와 Swing/Spring/Android 각 프레임워크가 이를 어떻게 구현하는지는 이론편의 “실무 프레임워크에서의 Observer 패턴”에서 이미 다뤘으므로 여기서 다시 설명하지 않는다. 이 실습에서는 그 이론을 과제 1의 StockSubject/StockObserver에 직접 적용해본다. java.util.Observable/Observer는 Java 9부터 @Deprecated이므로 새 코드에서는 사용하지 않는다.
구현 과제
Stock을 Model로 두고, 화면 출력만 담당하는StockView를StockObserver로 구현한다(과제 1의StockLogger와 동일한 인터페이스를 그대로 따른다).- Controller 역할의 클래스가 사용자 입력(예: 콘솔에서 받은 새 가격)을 받아
Stock.setPrice()를 호출하도록 구성하고,StockView는Stock만 관찰할 뿐 Controller를 직접 참조하지 않도록 한다. - 완성 후 Controller → Model → View로 이어지는 호출 흐름이 단방향인지, View를 교체해도 Model과 Controller 코드가 바뀌지 않는지 확인한다.
sequenceDiagram
participant User as "사용자"
participant Ctrl as "Controller"
participant Model as "Stock (Model)"
participant View as "StockView (Observer)"
User->>Ctrl: 새 가격 입력
Ctrl->>Model: setPrice(newPrice)
Model->>Model: notifyObservers()
Model->>View: update(symbol, price, change)
View->>View: 화면 갱신
Note over Ctrl,View: View는 Model만 관찰하고 Controller를 직접 참조하지 않는다
Spring Events
이론편에서 ApplicationEventPublisher/@EventListener를 이용한 발행-구독 메커니즘 자체는 이미 다뤘으므로 여기서 반복하지 않는다. 대신 실무에서 자주 발생하는 오개념 하나를 짚는다 — @EventListener는 리스너로 등록할 메서드에 붙여야 한다. @EventListener의 선언(@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}))은 클래스 선언에는 애초에 붙을 수 없도록 제한되어 있으므로, 클래스에 붙이면 “annotation type not applicable to this kind of declaration"라는 컴파일 오류로 즉시 드러난다 — 조용히 넘어가는 실수가 아니다.
실무에서 실제로 자주 발생하는, 훨씬 더 성가신 경우는 따로 있다. 메서드에 @EventListener를 정확히 붙였는데도 리스너가 등록되지 않는 경우인데, 원인은 그 클래스 자체가 Spring 빈으로 관리되지 않을 때다. 아래 EmailService는 문법적으로도 아무 문제가 없고 어노테이션 위치도 올바르지만, @Component(또는 다른 스테레오타입 어노테이션)가 없어 컴포넌트 스캔 대상이 아니므로 EventListenerMethodProcessor가 이 클래스를 아예 훑지 않는다. 그 결과 handleOrderCreated는 컴파일도 되고 어노테이션 위치도 맞지만 이벤트 리스너로 등록되지 않는다.
| |
| |
![Featured image of post [Design Patterns] 11. 옵저버: 이벤트 드리븐 아키텍처의 핵심 — 실습](/post/design-patterns/observer-event-driven-architecture-practice/wordcloud_hu_be04ff6d31e70e14.webp)
![[Design Patterns] 10. 브릿지와 플라이웨이트: 분리와 효율성 — 실습](/post/design-patterns/bridge-flyweight-separation-efficiency-practice/wordcloud_hu_9df51d59e6d2dd65.webp)
![[Design Patterns] 11. 옵저버: 이벤트 드리븐 아키텍처의 핵심](/post/design-patterns/observer-event-driven-architecture/wordcloud_hu_27d03deafc768928.webp)
![[Design Patterns] 11. 옵저버: 이벤트 드리븐 아키텍처의 핵심 — 실습](/post/design-patterns/observer-event-driven-architecture-practice/wordcloud_hu_d2b06bd99f92ab40.webp)
![[Design Patterns] 12. 전략과 상태: 알고리즘 캡슐화의 미학](/post/design-patterns/strategy-state-algorithm-encapsulation/wordcloud_hu_db50bb6f8a73da46.webp)
![[Design Patterns] 12. 전략과 상태: 알고리즘 캡슐화의 미학 — 실습](/post/design-patterns/strategy-state-algorithm-encapsulation-practice/wordcloud_hu_67a7d64650908a40.webp)
![[Hardware] LattePanda Alpha에 Ubuntu 16.04 LTS 설치 가이드](/post/2018-12-06-install-ubuntu-16.04-on-lattepanda/wordcloud_hu_fc536f8de2cbd4bf.webp)
![[Rust] Comprehensive Rust 무료 강의 정리 및 코스 구조](/post/2022-12-30-comprehensive-rust/wordcloud_hu_d1420ff38434cdb6.webp)
![[Concurrency Patterns] 00. 멀티스레딩 디자인 패턴 시리즈 소개와 커리큘럼](/post/multithreading-patterns/getting-started-multithreading-design-patterns/wordcloud_hu_2452ba41c5c18ed2.webp)