| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- 성능 개선
- 계산기
- 로드밸런서
- 코드카타
- 프록시 패턴
- 김영한
- 트러블슈팅
- 스케줄러
- 자바
- 템플릿 메서드 패턴
- 배치
- 백엔드
- 스프링 배치
- 빌더 패턴
- spring boot
- Spring
- 스프링
- Spring Batch
- 토스
- Til
- 이펙티브 자바
- GoF 23
- 추상클래스
- java
- lv1
- DB
- 프로그래머스
- Effective Java
- 디자인 패턴
- redis
- Today
- Total
김코딩
옵저버 패턴(Observer Pattern) 본문
디자인 패턴 시리즈의 마지막 주제는 옵저버 패턴(Observer Pattern)이다.
템플릿 메서드 패턴이 "알고리즘의 흐름을 정의"하는 거였다면, 옵저버 패턴은 "변화를 알려주는" 것이다.
예를 들어, 유튜브를 생각해보자. 좋아하는 채널을 구독하면, 그 채널에 새 영상이 올라올 때마다 알림이 온다. 채널은 구독자들에게 "새 영상 올렸어요!"라고 자동으로 알려준다.
옵저버 패턴은 이처럼 한 객체의 상태가 변하면, 그것을 지켜보던(Observe) 다른 객체들에게 자동으로 알려주는 패턴이다.
이번 글에서는 옵저버 패턴이 무엇인지, 왜 필요한지, 어떻게 구현하는지 알아보자.
옵저버 패턴의 정의
옵저버 패턴(Observer Pattern)은 객체의 상태 변화를 관찰하는 관찰자(Observer)들의 목록을 객체에 등록하여, 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 관찰자에게 통지하도록 하는 패턴이다.
핵심 특징:
- 일대다 의존성을 정의한다
- 주체(Subject)와 관찰자(Observer)로 구성된다
- 느슨한 결합(Loose Coupling)을 유지한다
- 발행-구독(Publish-Subscribe) 모델이다
현실 비유:
- 유튜브 구독 (채널이 새 영상 올리면 구독자들에게 알림)
- 신문 구독 (신문사가 신문 발행하면 구독자들에게 배달)
- 날씨 앱 (기상청이 날씨 변경하면 앱들에게 알림)
왜 옵저버 패턴이 필요할까?
날씨 정보를 제공하는 시스템을 개발한다고 가정해보자.
// 문제가 있는 코드
// 날씨 데이터
public class WeatherData {
private float temperature;
private float humidity;
// 날씨 변경 시 모든 디스플레이를 직접 업데이트
public void setMeasurements(float temperature, float humidity) {
this.temperature = temperature;
this.humidity = humidity;
// 문제: 디스플레이가 추가될 때마다 코드 수정 필요
CurrentConditionsDisplay display1 = new CurrentConditionsDisplay();
display1.update(temperature, humidity);
StatisticsDisplay display2 = new StatisticsDisplay();
display2.update(temperature, humidity);
ForecastDisplay display3 = new ForecastDisplay();
display3.update(temperature, humidity);
// 새로운 디스플레이 추가하려면? 또 코드 수정...
}
}
// 현재 날씨 디스플레이
public class CurrentConditionsDisplay {
public void update(float temperature, float humidity) {
System.out.println("현재 날씨: " + temperature + "도, 습도 " + humidity + "%");
}
}
// 통계 디스플레이
public class StatisticsDisplay {
public void update(float temperature, float humidity) {
System.out.println("평균 온도: " + temperature + "도");
}
}
문제점:
- 강한 결합: WeatherData가 모든 디스플레이 클래스를 알아야 함
- OCP 위반: 새로운 디스플레이 추가 시 WeatherData 수정 필요
- 유연성 부족: 런타임에 디스플레이를 추가/제거할 수 없음
- 확장 어려움: 디스플레이가 100개면? 코드가 엄청 길어짐
옵저버 패턴은 "관찰자들을 등록하고, 변화 시 자동으로 알림을 보내자"로 이 문제를 해결한다.
순수 Java로 구현하기
1단계: 인터페이스 정의
// 주체(Subject) 인터페이스
public interface Subject {
void registerObserver(Observer observer); // 관찰자 등록
void removeObserver(Observer observer); // 관찰자 제거
void notifyObservers(); // 관찰자들에게 알림
}
// 관찰자(Observer) 인터페이스
public interface Observer {
void update(float temperature, float humidity);
}
2단계: 주체(Subject) 구현
// 날씨 데이터 (주체)
public class WeatherData implements Subject {
private List<Observer> observers;
private float temperature;
private float humidity;
public WeatherData() {
this.observers = new ArrayList<>();
}
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
System.out.println("관찰자 등록됨");
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
System.out.println("관찰자 제거됨");
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(temperature, humidity);
}
}
// 날씨 데이터 변경
public void setMeasurements(float temperature, float humidity) {
this.temperature = temperature;
this.humidity = humidity;
notifyObservers(); // 자동으로 모든 관찰자에게 알림!
}
}
3단계: 관찰자(Observer) 구현
// 현재 날씨 디스플레이
public class CurrentConditionsDisplay implements Observer {
private String name;
public CurrentConditionsDisplay(String name) {
this.name = name;
}
@Override
public void update(float temperature, float humidity) {
System.out.println("[" + name + "] 현재 날씨: " + temperature + "도, 습도 " + humidity + "%");
}
}
// 통계 디스플레이
public class StatisticsDisplay implements Observer {
private float totalTemperature = 0;
private int count = 0;
@Override
public void update(float temperature, float humidity) {
totalTemperature += temperature;
count++;
float avg = totalTemperature / count;
System.out.println("[통계] 평균 온도: " + avg + "도");
}
}
// 예보 디스플레이
public class ForecastDisplay implements Observer {
@Override
public void update(float temperature, float humidity) {
if (temperature > 25) {
System.out.println("[예보] 날씨가 더워질 것 같습니다");
} else {
System.out.println("[예보] 날씨가 선선할 것 같습니다");
}
}
}
4단계: 사용 예시
public class Main {
public static void main(String[] args) {
// 주체 생성
WeatherData weatherData = new WeatherData();
// 관찰자 생성 및 등록
Observer display1 = new CurrentConditionsDisplay("디스플레이1");
Observer display2 = new StatisticsDisplay();
Observer display3 = new ForecastDisplay();
weatherData.registerObserver(display1);
weatherData.registerObserver(display2);
weatherData.registerObserver(display3);
System.out.println("\n=== 첫 번째 날씨 업데이트 ===");
weatherData.setMeasurements(25.5f, 65f);
System.out.println("\n=== 두 번째 날씨 업데이트 ===");
weatherData.setMeasurements(28.0f, 70f);
System.out.println("\n=== 관찰자 하나 제거 ===");
weatherData.removeObserver(display3);
System.out.println("\n=== 세 번째 날씨 업데이트 ===");
weatherData.setMeasurements(22.0f, 60f);
System.out.println("\n=== 새로운 관찰자 추가 ===");
Observer display4 = new CurrentConditionsDisplay("디스플레이4");
weatherData.registerObserver(display4);
System.out.println("\n=== 네 번째 날씨 업데이트 ===");
weatherData.setMeasurements(30.0f, 75f);
}
}

장점:
- WeatherData는 구체적인 디스플레이 클래스를 몰라도 됨
- 런타임에 관찰자 추가/제거 가능
- 새로운 디스플레이 추가해도 WeatherData 수정 불필요
실전 예제: 주식 가격 알림
// 주식 (주체)
public class Stock implements Subject {
private List<Observer> observers;
private String stockName;
private int price;
public Stock(String stockName, int initialPrice) {
this.stockName = stockName;
this.price = initialPrice;
this.observers = new ArrayList<>();
}
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(stockName, price);
}
}
public void setPrice(int price) {
System.out.println("\n📈 " + stockName + " 가격 변동: " + this.price + "원 → " + price + "원");
this.price = price;
notifyObservers();
}
}
// 투자자 (관찰자)
public class Investor implements Observer {
private String name;
private int targetPrice;
public Investor(String name, int targetPrice) {
this.name = name;
this.targetPrice = targetPrice;
}
@Override
public void update(String stockName, int price) {
if (price >= targetPrice) {
System.out.println("💰 [" + name + "] " + stockName + "이(가) 목표가(" + targetPrice + "원) 도달! 현재가: " + price + "원");
} else {
System.out.println("👀 [" + name + "] " + stockName + " 관찰 중... 현재가: " + price + "원");
}
}
}
// 알림 봇 (관찰자)
public class NotificationBot implements Observer {
@Override
public void update(String stockName, int price) {
System.out.println("🔔 [알림봇] " + stockName + " 가격 알림: " + price + "원");
}
}
// 사용
public class Main {
public static void main(String[] args) {
// 삼성전자 주식
Stock samsung = new Stock("삼성전자", 70000);
// 투자자들 등록
Investor investor1 = new Investor("김투자", 75000);
Investor investor2 = new Investor("이부자", 80000);
NotificationBot bot = new NotificationBot();
samsung.registerObserver(investor1);
samsung.registerObserver(investor2);
samsung.registerObserver(bot);
// 가격 변동
samsung.setPrice(72000);
samsung.setPrice(76000);
samsung.setPrice(81000);
}
}
실전 예제: 채팅방
// 채팅방 (주체)
public class ChatRoom implements Subject {
private List<Observer> users;
private String roomName;
public ChatRoom(String roomName) {
this.roomName = roomName;
this.users = new ArrayList<>();
}
@Override
public void registerObserver(Observer observer) {
users.add(observer);
System.out.println("👤 새로운 사용자가 [" + roomName + "]에 입장했습니다.");
}
@Override
public void removeObserver(Observer observer) {
users.remove(observer);
System.out.println("👋 사용자가 [" + roomName + "]에서 퇴장했습니다.");
}
@Override
public void notifyObservers(String message) {
for (Observer observer : users) {
observer.update(message);
}
}
public void sendMessage(String sender, String message) {
String fullMessage = "[" + sender + "] " + message;
System.out.println("\n💬 메시지 전송: " + fullMessage);
notifyObservers(fullMessage);
}
}
// 인터페이스 수정
interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers(String message);
}
interface Observer {
void update(String message);
}
// 사용자 (관찰자)
public class User implements Observer {
private String name;
public User(String name) {
this.name = name;
}
@Override
public void update(String message) {
System.out.println(" → " + name + "님이 수신: " + message);
}
public String getName() {
return name;
}
}
// 사용
public class Main {
public static void main(String[] args) {
// 채팅방 생성
ChatRoom chatRoom = new ChatRoom("디자인패턴 스터디");
// 사용자들
User user1 = new User("김도균");
User user2 = new User("이철수");
User user3 = new User("박영희");
// 입장
chatRoom.registerObserver(user1);
chatRoom.registerObserver(user2);
chatRoom.registerObserver(user3);
// 메시지 전송
chatRoom.sendMessage("김도균", "안녕하세요!");
chatRoom.sendMessage("이철수", "반갑습니다~");
// 퇴장
chatRoom.removeObserver(user3);
// 메시지 전송
chatRoom.sendMessage("김도균", "오늘 스터디 열심히 해봐요!");
}
}
Java의 내장 옵저버 (Deprecated)
Java에는 Observer 인터페이스와 Observable 클래스가 있었지만, Java 9부터 Deprecated 되었다.
// 예전 방식 (사용 비추천)
import java.util.Observable;
import java.util.Observer;
public class WeatherData extends Observable {
private float temperature;
public void setTemperature(float temperature) {
this.temperature = temperature;
setChanged(); // 변경 표시
notifyObservers(temperature); // 알림
}
}
Deprecated 이유:
- Observable이 클래스라서 상속 제한
- 스레드 안전하지 않음
- 이벤트 모델이 제한적
대신 사용:
- 직접 구현 (위 예시처럼)
- Spring의 ApplicationEvent
- RxJava, Flow API
옵저버 패턴의 장점
- 느슨한 결합: 주체와 관찰자가 독립적
- OCP 준수: 새로운 관찰자 추가 시 주체 수정 불필요
- 동적 구독: 런타임에 관찰자 추가/제거 가능
- 브로드캐스트: 한 번에 여러 객체에게 알림
옵저버 패턴의 단점
- 알림 순서: 관찰자들의 알림 순서를 보장할 수 없음
- 메모리 누수: 관찰자를 제거하지 않으면 메모리 누수 발생 가능
- 예상치 못한 업데이트: 관찰자가 많으면 성능 저하
- 순환 의존성: 관찰자가 주체를 업데이트하면 무한 루프
Spring에서의 옵저버 패턴
Spring의 이벤트 시스템이 옵저버 패턴을 사용한다.
// 이벤트 정의
public class OrderCreatedEvent {
private Long orderId;
private int amount;
public OrderCreatedEvent(Long orderId, int amount) {
this.orderId = orderId;
this.amount = amount;
}
public Long getOrderId() { return orderId; }
public int getAmount() { return amount; }
}
// 이벤트 발행자 (주체)
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
public OrderService(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public void createOrder(Long orderId, int amount) {
// 주문 생성 로직
System.out.println("주문 생성: " + orderId);
// 이벤트 발행
eventPublisher.publishEvent(new OrderCreatedEvent(orderId, amount));
}
}
// 이벤트 리스너 (관찰자)
@Component
public class EmailNotificationListener {
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
System.out.println("📧 이메일 발송: 주문번호 " + event.getOrderId());
}
}
@Component
public class SmsNotificationListener {
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
System.out.println("📱 SMS 발송: 주문번호 " + event.getOrderId());
}
}
@Component
public class PointRewardListener {
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
int points = event.getAmount() / 100;
System.out.println("⭐ 포인트 적립: " + points + "점");
}
}
사용:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(Application.class, args);
OrderService orderService = context.getBean(OrderService.class);
orderService.createOrder(1L, 50000);
}
}
실무에서 언제 사용할까?
사용하면 좋은 경우:
- 한 객체의 변경이 다른 객체들에게 영향을 줄 때
- 어떤 객체들이 영향을 받을지 미리 알 수 없을 때
- 이벤트 기반 시스템을 만들 때
- GUI 프로그래밍 (버튼 클릭 이벤트 등)
사용하지 않아도 되는 경우:
- 관찰자가 하나뿐일 때
- 실시간 알림이 필요 없을 때
- 강한 결합이 문제되지 않을 때
핵심 원칙:
"한 객체의 변화를 여러 객체에게 자동으로 알려야 한다면 옵저버 패턴을 고려하라"
결론
옵저버 패턴은 객체 간의 느슨한 결합을 유지하면서 상태 변화를 자동으로 알려주는 패턴이다.
기억해야 할 포인트:
- 일대다 관계를 정의한다
- 느슨한 결합을 유지한다
- 발행-구독 모델이다
- Spring의 이벤트 시스템이 대표적
- 런타임에 구독/해지 가능
이것으로 디자인 패턴 시리즈를 마무리한다. 지금까지 배운 11가지 패턴은 실무에서 가장 많이 사용되는 핵심 패턴들이다. 모든 패턴을 외울 필요는 없지만, 각 패턴이 어떤 문제를 해결하는지 이해하고, 적절한 상황에서 사용할 수 있다면 훨씬 더 나은 코드를 작성할 수 있을 것이다.
디자인 패턴 학습의 핵심:
- 패턴을 암기하지 말고 이해하라
- 무조건 사용하지 말고 필요할 때만 사용하라
- 간단한 문제에 복잡한 패턴을 쓰지 마라 (오버엔지니어링 주의)
- 실제 프로젝트에 적용하며 배워라
'디자인 패턴' 카테고리의 다른 글
| 템플릿 메서드 패턴(Template Method Pattern) (1) | 2026.01.14 |
|---|---|
| 전략 패턴(Strategy Pattern) (0) | 2026.01.14 |
| 프록시 패턴(Proxy Pattern) (0) | 2026.01.13 |
| 데코레이터 패턴(Decorator Pattern) (0) | 2026.01.13 |
| 어댑터 패턴(Adapter Pattern) (0) | 2026.01.13 |