13-7. 옵저버 패턴 (Observer Pattern)

박은서's avatar
Feb 24, 2026
13-7. 옵저버 패턴 (Observer Pattern)

1. 옵저버 패턴 (Observer Pattern)

1️⃣ 옵저버 패턴이란?

1) 개념

한 객체(Subject)의 상태(state)가 변경되면, 해당 객체에 등록된 여러 Observer 객체들에게 자동으로 통지(notify)하는 구조
⚠️ Java에서는 인터페이스 기반 설계로 구현하는 것이 일반적

2️⃣ Java에서의 역할 구조

1) Subject (관찰 대상)

  • Observer 목록을 관리
  • register(), remove(), notifyObservers() 제공

2) Observer (관찰자)

  • update() 메서드를 정의
  • Subject가 상태 변경 시 호출됨

3️⃣ 구조 다이어그램 (개념)

+----------------+ | Subject | +----------------+ | + register() | | + remove() | | + notify() | +----------------+ ▲ | +----------------+ | ConcreteSubject| +----------------+ +----------------+ | Observer | +----------------+ | + update() | +----------------+ ▲ | +----------------+ |ConcreteObserver| +----------------+

4️⃣ 코드 예시 (정석 구현)

1) Observer 인터페이스

public interface Observer { void update(int state); }

2) Subject 인터페이스

public interface Subject { void registerObserver(Observer observer); void removeObserver(Observer observer); void notifyObservers(); }

3) ConcreteSubject

import java.util.ArrayList; import java.util.List; public class ConcreteSubject implements Subject { private List<Observer> observers = new ArrayList<>(); private int state; @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(state); } } public void setState(int state) { this.state = state; notifyObservers(); } }

4) ConcreteObserver

public class ConcreteObserver implements Observer { private String name; public ConcreteObserver(String name) { this.name = name; } @Override public void update(int state) { System.out.println(name + " notified. New state: " + state); } }

5) 실행 코드

public class Main { public static void main(String[] args) { ConcreteSubject subject = new ConcreteSubject(); Observer observer1 = new ConcreteObserver("Observer1"); Observer observer2 = new ConcreteObserver("Observer2"); subject.registerObserver(observer1); subject.registerObserver(observer2); subject.setState(10); } }

6) 출력

Observer1 notified. Newstate:10 Observer2 notified. Newstate:10

5️⃣ Java에서 중요한 포인트

1) 인터페이스 기반 설계

구현이 아닌 추상화에 의존 (DIP 원칙)

2) 느슨한 결합 (Loose Coupling)

Subject는 Observer의 구체 클래스 모름
Observer 타입만 알면 됨

3) OCP (Open-Closed Principle)

Observer를 추가해도 기존 Subject 코드 수정 없음

6️⃣ Java 표준 라이브러리의 옵저버

  • Java 8 이전
    • java.util.Observer
    • java.util.Observable
    • ➡️ 존재했지만 Java 9부터 deprecated 되었습니다.
      이유
    • 상속 강제 (Observable은 클래스)
    • 유연하지 않음
    • 인터페이스 기반이 아님
  • 실무에서는 직접 구현하거나, 아래 기술을 사용
    • Spring EventListener
    • RxJava
    • Reactor (Flux, Mono)
    • 이벤트 버스 라이브러리

7️⃣ 실무에서의 대표 사용 예

1) Spring 이벤트 시스템

@EventListener public void handleEvent(MyEvent event) { ... }
→ 내부적으로 옵저버 패턴 기반

2) GUI 이벤트 리스너

button.addActionListener(e -> { System.out.println("Clicked!"); });
→ 버튼 상태 변화 → 리스너 호출

8️⃣ 옵저버 vs Pub-Sub 차이 (Java 관점)

구분
Observer
Pub-Sub
연결 구조
Subject가 Observer 직접 참조
중간에 메시지 브로커 존재
결합도
약하지만 직접 연결
완전 분리
Swing 이벤트
Kafka, Redis Pub/Sub

9️⃣ 단점

  • Observer가 많으면 성능 문제
  • 순환 참조 위험
  • 멀티스레드 환경에서 동기화 필요

2. 실습(ex07)

1️⃣ polling (패턴X 옵저버가 뭔지)

1) LotteMart.java

C:\workspace\java_lab\designapp\src\ex07\polling\LotteMart.java
package ex07.polling; public class LotteMart { private String value = null; // 상태 확인 public String getValue() { return value; } // 입고 public void received() { value = "상품"; } }

2) Customer1.java

C:\workspace\java_lab\designapp\src\ex07\polling\Customer1.java
package ex07.polling; public class Customer1 { // 상품 있어? public void request(LotteMart lotteMart) { String value = lotteMart.getValue(); if (value != null) update(value); } // 있으면 알림 받기 public void update(String msg) { System.out.println("손님1이 받은 알림 : " + msg); } }

3) App.java

C:\workspace\java_lab\designapp\src\ex07\polling\App.java
package ex07.polling; public class App { public static void main(String[] args) { LotteMart lm = new LotteMart(); Customer1 cus1 = new Customer1(); // 1. 마트에 상품 입고 확인 new Thread(() -> { for (int i = 1; i < 11; i++) { System.out.println("입고중...(" + i + "초)"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } lm.received(); System.out.println("입고완료!!"); }).start(); // 2. 손님이 풀링 } }

4) 결과

notion image

5) App.java

package ex07.polling; public class App { public static void main(String[] args) { LotteMart lm = new LotteMart(); Customer1 cus1 = new Customer1(); // 1. 마트에 상품 입고 확인 (마트 스레드) new Thread(() -> { // 10초 대기 for (int i = 1; i < 11; i++) { System.out.println("입고중...(" + i + "초)"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } // 입고 완료 lm.received(); System.out.println("입고완료!!"); }).start(); // 2. 손님이 풀링 (손님 스레드) new Thread(() -> { // 3초에 한번씩 상품 확인 요청 while (true) { System.out.println("[손님] 상품 있어?"); cus1.request(lm); try { Thread.sleep(3000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }).start(); } }

6) 결과

notion image

2️⃣ push (옵저버 패턴)

1) Coustomer.java (인터페이스)

C:\workspace\java_lab\designapp\src\ex07\push\sub\Customer.java
package ex07.push.sub; public interface Customer { void update(String msg); }

2) Mart.java (인터페이스)

C:\workspace\java_lab\designapp\src\ex07\push\pub\Mart.java
package ex07.push.pub; import ex07.push.sub.Customer; public interface Mart { // 1. 구독 등록 void add(Customer customer); // 2. 출판(입고) void received(); // 3. 알림 (구독자 update 호출) void notify(String msg); // 4. 구독 취소 void remove(Customer customer); }

3) Cus1.java

C:\workspace\java_lab\designapp\src\ex07\push\sub\Cus1.java
package ex07.push.sub; public class Cus1 implements Customer{ @Override public void update(String msg) { System.out.println("손님1이 받은 알림 : " + msg); } }

4) LotteMart.java

C:\workspace\java_lab\designapp\src\ex07\push\pub\LotteMart.java
package ex07.push.pub; import ex07.push.sub.Customer; import java.util.ArrayList; import java.util.List; public class LotteMart implements Mart{ private String value = null; // 상품(1개로 전제) // 임계영역(여러 스레드가 동시접근이 일어나게 될 때) -> 레이스 컨디션 private List<Customer> list = new ArrayList<>(); // 구독자 명단 // synchronized를 붙이면 하나의 스레드가 접근하면 다른 스레드가 접근하지 못하도록 막음 (레이스 컨디션 안 걸릴 수 있음) @Override synchronized public void add(Customer customer) { list.add(customer); } @Override public void received() { value = "바나나"; // 입고 완료 notify(value); } @Override public void notify(String msg) { list.forEach(customer -> { // 1. 구독에 따라 다르게 분기해줘야 함 customer.update(msg); // push }); } @Override public void remove(Customer customer) { list.remove(customer); } }

[참고] 크리티컬 섹션 (Critical Section, 임계영역)

[참고] 크리티컬 섹션 (Critical Section, 임계영역)

5) Cus2.java

C:\workspace\java_lab\designapp\src\ex07\push\sub\Cus2.java
package ex07.push.sub; public class Cus2 implements Customer{ @Override public void update(String msg) { System.out.println("손님2이 받은 알림 : " + msg); } }

6) App.java

C:\workspace\java_lab\designapp\src\ex07\push\App.java
package ex07.push; import ex07.push.pub.LotteMart; import ex07.push.pub.Mart; import ex07.push.sub.Cus1; import ex07.push.sub.Cus2; import ex07.push.sub.Customer; /** * push 방식 (옵저버패턴) * 1. 구현방식 정해져 있음 * 2. 자바 lib : Reactive Java (자바9), Stomp(그 외) * 3. 스프링 : Stomp(WebSocket 라이브러리) */ public class App { public static void main(String[] args) { // 1. 객체 생성 Mart lottemart = new LotteMart(); Customer cus1 = new Cus1(); Customer cus2 = new Cus2(); // 2. 구독 lottemart.add(cus1); lottemart.add(cus2); // 3. 구독취소 lottemart.remove(cus1); // 4. 출판 new Thread(() -> { lottemart.received(); }).start(); } }
notion image
package ex07.push; import ex07.push.pub.LotteMart; import ex07.push.pub.Mart; import ex07.push.sub.Cus1; import ex07.push.sub.Cus2; import ex07.push.sub.Customer; /** * push 방식 (옵저버패턴) * 1. 구현방식 정해져 있음 * 2. 자바 lib : Reactive Java (자바9), Stomp(그 외) * 3. 스프링 : Stomp(WebSocket 라이브러리) */ public class App { public static void main(String[] args) { // 1. 객체 생성 Mart lottemart = new LotteMart(); Customer cus1 = new Cus1(); Customer cus2 = new Cus2(); // 2. 구독 lottemart.add(cus1); System.out.println("손님1 구독됨"); lottemart.add(cus2); System.out.println("손님2 구독됨"); // 3. 구독취소 lottemart.remove(cus1); System.out.println("손님1 구독 취소"); // 4. 출판 System.out.println("롯데마트 입고 시작"); lottemart.received(); } }
notion image
Share article