Design pattern 9. Observer Pattern

김미숙's avatar
Jul 23, 2025
Design pattern 9. Observer Pattern
💡
Observer: 관찰자
가장 중요한 패턴
Observer Pattern을 알면 Spring Boot Webflux를 구현할 수 있다
Webflux
notion image
 

✅ 1. Observer Pattern란?

  • 소프트웨어 디자인 패턴 중 하나로, 어떤 객체의 상태 변화가 생기면, 그 객체를 구독한 다른 객체들에게 자동으로 알림을 보내는 방식
  • 객체 간 1:N 관계를 정의해서, 한 객체의 상태 변화가 있을 때 그에 의존하는 객체들(옵서버)에게 자동으로 알림이 가도록 만드는 행동(Behavioral) 디자인 패턴
  • 발행자(Subject)와 구독자(Observer) 간의 일대다 관계를 정의해, 발행자의 상태 변화가 있을 때 모든 구독자에게 알림을 전파하는 패턴
 

✅ 구성 요소

역할
설명
Subject (발행자)
상태를 가지고 있고, 옵저버 등록/해제/알림 기능을 제공
Observer (구독자)
Subject의 상태 변화에 따라 반응하는 객체
ConcreteSubject
실제 상태를 가진 구체적인 발행자
ConcreteObserver
실제 알림을 받고 처리하는 구체적인 옵저버
 

✅ 동작 흐름

  1. 옵저버는 subject.attach(observer) 방식으로 구독함
  1. subject 상태가 변경되면 notifyObservers() 호출
  1. 등록된 옵저버들의 update() 메서드가 실행됨
 

✅ 자바 스타일 예제

1. 인터페이스 정의
// 옵저버 public interface Observer { void update(String message); } // 발행자 public interface Subject { void attach(Observer observer); void detach(Observer observer); void notifyObservers(String message); }
2. 구현 클래스
public class NewsPublisher implements Subject { private List<Observer> observers = new ArrayList<>(); public void attach(Observer observer) { observers.add(observer); } public void detach(Observer observer) { observers.remove(observer); } public void notifyObservers(String message) { for (Observer o : observers) { o.update(message); } } // 예: 뉴스 발행 public void publish(String news) { System.out.println("📰 뉴스 발행: " + news); notifyObservers(news); } }
public class EmailSubscriber implements Observer { private String email; public EmailSubscriber(String email) { this.email = email; } public void update(String message) { System.out.println("📧 [" + email + "] 뉴스 수신: " + message); } }
3. 사용 예
NewsPublisher publisher = new NewsPublisher(); publisher.attach(new EmailSubscriber("user1@example.com")); publisher.attach(new EmailSubscriber("user2@example.com")); publisher.publish("옵저버 패턴 완전정복!");
 

✅ 장점

장점
설명
느슨한 결합(Loosely Coupled)
Subject는 Observer의 구체 구현을 몰라도 됨
동적으로 옵저버 추가/제거 가능
유연성↑
이벤트 기반 시스템에 적합
GUI, 채팅, 알림 등에서 활용
 

❌ 단점

단점
설명
많은 옵저버가 있을 경우 성능 저하
notifyObservers() 비용 발생
디버깅 어려움
어떤 옵저버가 반응했는지 추적 어려움
순서 보장 어려움
옵저버 알림 순서에 대한 제어 없음 (보통 FIFO지만 명시는 안 됨)
 

✅ 실전 예시

예시
설명
Java Swing 이벤트 처리
버튼 클릭 → 리스너 등록
Spring ApplicationEventPublisher
이벤트 발행 → 여러 핸들러가 처리
게시판 알림 시스템
댓글 달림 → 작성자에게 알림
 

✅ 요약

항목
내용
핵심 개념
한 객체의 상태 변화 → 여러 객체에 자동 통보
구조
Subject ↔ Observer 인터페이스 기반
특징
느슨한 결합, 이벤트 기반, 유연한 확장
 

옵저버 패턴에서의 Polling 방식과 Push 방식

  • 발행자(Subject)의 상태 변화가 옵저버(Observer)에게 어떻게 전달되는가에 따라 구분된다.
 

✅ 핵심 차이 요약

구분
Polling 방식
Push 방식
주도권
옵저버가 주도
발행자(Subject)가 주도
데이터 흐름
옵저버가 주기적으로 물어봄
발행자가 상태가 바뀌면 즉시 알림
구조
observer.getState(subject)
subject.notify(observer, newState)
실시간성
낮음
높음
리소스 사용
불필요한 요청 많음
이벤트 발생 시만 동작
 

✅ 옵저버 패턴에서의 두 방식 비교

🔹 1. Polling 방식
💡
Polling 방식에서 가장 중요한 것은 요청 주기 (얼마에 한 번씩 요청할 것인가)
✅ 폴링 주기 설정 시 고려할 요소
요소
설명
🕒 실시간성
사용자에게 얼마나 빠르게 변화가 반영되어야 하는가?
⚙️ 서버 부하
짧은 주기는 서버에 많은 요청 유발, 트래픽 증가
🌐 네트워크 환경
클라이언트가 모바일/저속망일 경우 과도한 요청은 비효율
🧠 데이터 변경 빈도
데이터가 자주 바뀌면 짧게, 자주 안 바뀌면 길게 설정
🔋 배터리/리소스
모바일 앱이라면 짧은 주기는 배터리 소모 증가
✅ 실무에서의 권장 설정 예시
기능
권장 폴링 주기
🟢 실시간 채팅
1~3초 (→ WebSocket 권장)
🔔 알림 수신
10~30초
📋 게시글/댓글 새로고침
20~60초
📈 실시간 통계 차트
5~15초
🛒 주문 처리 대시보드
3~10초
📦 배송 상태 조회
1분 이상
💾 데이터 동기화
1~5분
✅ 전략적 조절 방법
1. 초기 짧게 → 점점 늘리기 (Adaptive Polling)
  • 데이터가 자주 안 바뀌면, 점점 느리게 폴링
  • 변화가 감지되면 주기를 다시 짧게 조정
long interval = 1000; // 시작: 1초 while (true) { boolean changed = check(); // 변화 감지 if (!changed) interval = Math.min(interval * 2, 30000); // 최대 30초 else interval = 1000; Thread.sleep(interval); }
2. 유휴 상태일 때 폴링 중단 또는 느리게
  • 사용자가 다른 탭으로 이동하거나 비활성 상태면 → 폴링 간격을 크게 늘리거나 중지
3. 서버가 변화 시 알려주는 방식으로 전환
  • 폴링은 기본, 하지만 서버가 변화 발생 시 직접 푸시(Webhook, SSE, WebSocket)로 통보하는 구조 도입
✅ 결정 팁 요약
조건
추천 주기
데이터가 자주 바뀌고 사용자도 즉시 반응하길 원함
1~3초
중요하지만 즉각 반응 필요 없음
10~30초
서버 리소스가 부족함
1분 이상, 변화 적은 곳만 폴링
모바일 환경
배터리 고려해 30초~1분 이상
✅ 마무리 요약
"짧을수록 반응성 좋고, 길수록 자원 효율이 좋다."
→ 실시간성 vs 리소스 효율성 트레이드오프를 고려해서 상황별로 조정하는 것이 핵심입니다.
옵저버가 주기적으로 발행자의 상태를 확인함
📌 코드 흐름
LotteMart
package ex08.polling; public class LotteMart { private String value = null; public String getValue() { return value; } public void received() { for (int i = 0; i < 5; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } value = "상품"; } }
Thread
notion image
Customer1
package ex08.polling; public class Customer1 { public void update(String msg) { System.out.println("손님1이 받은 알림: " + msg); } }
App
package ex08.polling; public class App { public static void main(String[] args) { LotteMart lotteMart = new LotteMart(); Customer1 customer1 = new Customer1(); // 1. 마트는 입고 준비 (5초 걸림 - 별도 쓰레드) // 마트 입고 작업은 백그라운드 스레드에서 수행 // 별도의 스레드로 돌리기 때문에, main 쓰레드에서 계속 입고 상태를 확인가능 new Thread(() -> { lotteMart.received(); }).start(); // 2. 입고 확인 (데몬) - while문으로 계속 상태 확인 (100ms마다) // 메인 쓰레드에서 100ms마다 상태 확인 (폴링 while (true) { try { Thread.sleep(100); // 주기적인 검사 (폴링 간격) - 0.1초마다 lotteMart.getValue() 호출해서 입고 여부 확인, 값이 null이면 계속 기다림 } catch (InterruptedException e) { throw new RuntimeException(e); } // 값이 설정되면 알림 보내고 루프 종료 if (lotteMart.getValue() != null) { // request (polling <- 데몬으로 돌아서 계속 요청하니까 다른 요청을 받을 수가 없다 customer1.update(lotteMart.getValue() + "이 들어왔습니다."); // 상품 도착 알림 받고 break break; } else { System.out.println("상품이 아직 들어오지 않았습니다"); } } } }
✅ 특징 요약
항목
설명
🔁 Polling 방식
while (true) 루프로 계속 마트 상태를 확인함 (lotteMart.getValue())
🕰️ 주기적 확인
Thread.sleep(100)으로 100ms 간격으로 상태 확인 → 폴링 주기
🧵 멀티스레드 구조
입고 작업(received())은 별도 스레드에서 수행 → 메인 스레드가 폴링 가능
🧠 단순한 감시 로직
값이 null인지 아닌지만 확인 → 상태 감지 조건이 명확함
🔒 상태 직접 접근
옵저버가 Subject 상태를 직접 확인하는 구조 → Push가 아닌 Pull
💬 출력 메시지 분기
입고 전에는 "상품이 아직 들어오지 않았습니다" 반복 출력, 입고 후 "~이 들어왔습니다" 출력
🛑 한 번만 알림 후 종료
상품이 들어오면 update() 한 번 호출하고 break로 루프 종료함 (반복 감지 X)
✅ 장점
장점
설명
구현이 간단함
복잡한 이벤트 리스너 없이 상태만 보면 됨
병렬 구조 가능
Thread로 입고 처리와 감시를 동시에 수행
제어권 유지
감지 타이밍과 주기 설정이 명확함 (sleep()으로 직접 제어 가능)
❌ 단점
단점
설명
CPU 리소스 낭비
계속 상태를 확인하기 때문에, 의미 없는 반복이 발생 가능
실시간성 한계
100ms보다 빠른 반응은 불가능 (sleep 주기에 의존)
비효율적인 구조
상태가 거의 안 바뀌는 경우에도 계속 확인 요청 발생
Observer 패턴 구조와 다름
진짜 옵저버(Push 구조)가 아니라, 상태 직접 확인(Pull 구조)
 
🔹 2. Push 방식
발행자의 상태가 변경되면 옵저버에게 즉시 알려줌
📌 코드 흐름
EMart
package ex08.push.pub; import ex08.push.sub.Customer; import java.util.ArrayList; import java.util.List; // 구현할 때는 각 메서드의 책임을 알아야 한다 public class EMart implements Mart { // 구독자 명단 private List<Customer> customerList = new ArrayList<Customer>(); // 구독 등록 @Override public void add(Customer customer) { customerList.add(customer); } // 구독 취소 @Override public void remove(Customer customer) { customerList.remove(customer); } // 출판 @Override public void receive() { for (int i = 0; i < 5; i++) { System.out.println("."); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } notify("EMart : 딸기"); // 출판 완료 후 고객에게 알림 <- callback 후 push } // 알림 @Override public void notify(String msg) { for (Customer customer : customerList) { customer.update(msg); } } }
LotteMart
package ex08.push.pub; import ex08.push.sub.Customer; import java.util.ArrayList; import java.util.List; // 구현할 때는 각 메서드의 책임을 알아야 한다 public class LotteMart implements Mart { // 구독자 명단 private List<Customer> customerList = new ArrayList<Customer>(); // 구독 등록 @Override public void add(Customer customer) { customerList.add(customer); } // 구독 취소 @Override public void remove(Customer customer) { customerList.remove(customer); } // 출판 @Override public void receive() { for (int i = 0; i < 5; i++) { System.out.println("."); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } notify("LotteMart : 바나나"); // 출판 완료 후 고객에게 알림 <- callback 후 push } // 알림 @Override public void notify(String msg) { for (Customer customer : customerList) { customer.update(msg); } } }
→ 발행자(Subject, 여기선 EMart)가 상태 변화(=딸기 입고)를 감지하고, 즉시 옵저버(Customer)들에게 알리는 구조
  • 발행자인 EMart가 딸기 입고를 시뮬레이션 (5초 기다림)
  • 그 후 notify() 호출 → 상태 변화 발생 시점
  • 등록된 모든 옵저버(Customer)에게 즉시 메시지를 푸시
Cus1
package ex08.push.sub; public class Cus1 implements Customer { @Override public void update(String msg) { System.out.println("손님1이 받은 알림 : " + msg); } }
Cus2
package ex08.push.sub; public class Cus2 implements Customer { @Override public void update(String msg) { System.out.println("손님2이 받은 알림 : " + msg); } }
App
package ex08.push; import ex08.push.pub.EMart; import ex08.push.pub.LotteMart; import ex08.push.sub.Cus1; import ex08.push.sub.Cus2; public class App { public static void main(String[] args) { // 1. 객체 초기화 LotteMart lotteMart = new LotteMart(); EMart emart = new EMart(); Cus1 cus1 = new Cus1(); Cus2 cus2 = new Cus2(); // 2. 구독 lotteMart.add(cus1); lotteMart.add(cus2); emart.add(cus1); emart.add(cus2); // 3. 구독 취소 lotteMart.remove(cus2); // 4. 출판 (출판을 누가 할지는 나중에 정하면 됨) new Thread(() -> { lotteMart.receive(); }).start(); new Thread(() -> { emart.receive(); }).start(); } }
notion image
 

✅ 예로 쉽게 이해하기

상황
Polling 방식
Push 방식
택배 배송 조회
내가 계속 배송조회 누름
택배사에서 "배송됨" 문자 보냄
메일 앱
5분마다 서버에 메일 있나 확인
새 메일 도착 시 즉시 알림
게임 채팅
내가 계속 새 메시지 왔나 확인
상대가 채팅 보내면 바로 알림
 

✅ 실제 시스템 예시

시스템
Polling
Push
HTTP polling
브라우저가 주기적으로 Ajax 요청
X
WebSocket
X
서버가 클라이언트에게 직접 메시지 전송
RSS 리더
클라이언트가 10분마다 피드 확인
X
Firebase Cloud Messaging
X
서버가 클라이언트에게 푸시 알림
 

✅ 요약 정리

항목
Polling (Pull)
Push
주체
옵저버가 상태를 "가져감"
발행자가 상태를 "보냄"
타이밍
주기적
이벤트 발생 시
효율성
낮음 (리소스 낭비)
높음
구현
간단
상대적으로 복잡
옵저버 패턴에선?
pull 기반 옵저버도 존재
전통적인 옵저버 구조는 대부분 push 기반
 
Share article

parangdajavous