김코딩

옵저버 패턴(Observer Pattern) 본문

디자인 패턴

옵저버 패턴(Observer Pattern)

김코딩딩 2026. 1. 14. 13:02

디자인 패턴 시리즈의 마지막 주제는 옵저버 패턴(Observer Pattern)이다.

 

템플릿 메서드 패턴"알고리즘의 흐름을 정의"하는 거였다면, 옵저버 패턴"변화를 알려주는" 것이다.

 

예를 들어, 유튜브를 생각해보자. 좋아하는 채널을 구독하면, 그 채널에 새 영상이 올라올 때마다 알림이 온다. 채널은 구독자들에게 "새 영상 올렸어요!"라고 자동으로 알려준다.

 

옵저버 패턴은 이처럼 한 객체의 상태가 변하면, 그것을 지켜보던(Observe) 다른 객체들에게 자동으로 알려주는 패턴이다.

 

이번 글에서는 옵저버 패턴무엇인지, 왜 필요한지, 어떻게 구현하는지 알아보자.


옵저버 패턴의 정의

옵저버 패턴(Observer Pattern)객체의 상태 변화를 관찰하는 관찰자(Observer)들의 목록을 객체에 등록하여, 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 관찰자에게 통지하도록 하는 패턴이다.

 

핵심 특징:

  1. 일대다 의존성을 정의한다
  2. 주체(Subject)와 관찰자(Observer)로 구성된다
  3. 느슨한 결합(Loose Coupling)을 유지한다
  4. 발행-구독(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 + "도");
    }
}

문제점:

  1. 강한 결합: WeatherData가 모든 디스플레이 클래스를 알아야 함
  2. OCP 위반: 새로운 디스플레이 추가 시 WeatherData 수정 필요
  3. 유연성 부족: 런타임에 디스플레이를 추가/제거할 수 없음
  4. 확장 어려움: 디스플레이가 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

옵저버 패턴의 장점

  1. 느슨한 결합: 주체와 관찰자가 독립적
  2. OCP 준수: 새로운 관찰자 추가 시 주체 수정 불필요
  3. 동적 구독: 런타임에 관찰자 추가/제거 가능
  4. 브로드캐스트: 한 번에 여러 객체에게 알림

옵저버 패턴의 단점

  1. 알림 순서: 관찰자들의 알림 순서를 보장할 수 없음
  2. 메모리 누수: 관찰자를 제거하지 않으면 메모리 누수 발생 가능
  3. 예상치 못한 업데이트: 관찰자가 많으면 성능 저하
  4. 순환 의존성: 관찰자가 주체를 업데이트하면 무한 루프

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가지 패턴은 실무에서 가장 많이 사용되는 핵심 패턴들이다. 모든 패턴을 외울 필요는 없지만, 각 패턴이 어떤 문제를 해결하는지 이해하고, 적절한 상황에서 사용할 수 있다면 훨씬 더 나은 코드를 작성할 수 있을 것이다.

 

디자인 패턴 학습의 핵심:

  • 패턴을 암기하지 말고 이해하라
  • 무조건 사용하지 말고 필요할 때만 사용하라
  • 간단한 문제에 복잡한 패턴을 쓰지 마라 (오버엔지니어링 주의)
  • 실제 프로젝트에 적용하며 배워