Observer: 관찰자
가장 중요한 패턴
Observer Pattern을 알면 Spring Boot Webflux를 구현할 수 있다
Webflux
‣

✅ 1. Observer Pattern란?
- 소프트웨어 디자인 패턴 중 하나로, 어떤 객체의 상태 변화가 생기면, 그 객체를 구독한 다른 객체들에게 자동으로 알림을 보내는 방식
- 객체 간 1:N 관계를 정의해서, 한 객체의 상태 변화가 있을 때 그에 의존하는 객체들(옵서버)에게 자동으로 알림이 가도록 만드는 행동(Behavioral) 디자인 패턴
- 발행자(Subject)와 구독자(Observer) 간의 일대다 관계를 정의해, 발행자의 상태 변화가 있을 때 모든 구독자에게 알림을 전파하는 패턴
✅ 구성 요소
역할 | 설명 |
Subject (발행자) | 상태를 가지고 있고, 옵저버 등록/해제/알림 기능을 제공 |
Observer (구독자) | Subject의 상태 변화에 따라 반응하는 객체 |
ConcreteSubject | 실제 상태를 가진 구체적인 발행자 |
ConcreteObserver | 실제 알림을 받고 처리하는 구체적인 옵저버 |
✅ 동작 흐름
- 옵저버는
subject.attach(observer)
방식으로 구독함
- subject 상태가 변경되면
notifyObservers()
호출
- 등록된 옵저버들의
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

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();
}
}

✅ 예로 쉽게 이해하기
상황 | 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