📡 옵저버 패턴 (Observer Pattern)

2025. 9. 9. 16:01·Computer Science/Design Pattern

주체(Subject)가 어떤 객체(Observer)의 상태 변화를 관찰하다가, 상태 변화가 생길 때마다 메서드 등을 통해 옵저버 목록에 있는 모든 옵저버(Observer)에게 변화를 자동으로 알려주는 디자인 패턴입니다.

 

주체와 객체를 따로 두지 않고, 상태가 변경되는 객체(주체)를 기반으로 구축하기도 합니다. 대표적인 예로 트위터(유튜브)의 구독 알림 기능이나 MVC(Model-View-Controller) 패턴 등이 있습니다.


📁 비유: 유튜브 채널 구독 (주체와 구독자)

옵저버 패턴은 유튜브 채널과 구독자 관계를 생각하면 가장 이해하기 쉽습니다.

  • 주체 (Subject): '얄팍한 코딩사전' 유튜브 채널 (Java 예제의 Topic 클래스)
  • 옵저버 (Observers): 채널을 구독한 구독자 A, B, C (Java 예제의 TopicSubscriber 객체들)

작동 방식:

  1. register(Observer): 구독자 A, B, C가 "구독" 버튼을 누릅니다. (주체는 이들을 observers 리스트에 추가합니다.)
  2. unregister(Observer): 구독자 C가 "구독 취소" 버튼을 누릅니다. (리스트에서 제거됩니다.)
  3. 상태 변화 (State Change): 유튜버(주체)가 새 영상("아무무는 OP입니다!!")을 업로드합니다. ( postMessage() 호출 )
  4. notifyObservers(): 주체는 자신이 누구를 구독했는지 알 필요 없이 그냥 "구독자 목록" 전체에 알림을 발송합니다.
  5. update(): 알림을 받은 구독자 A, B는 "새 영상이 올라왔군!"이라는 알림(업데이트)을 각자 받게 됩니다.

이 구조의 핵심은 느슨한 결합(Loose Coupling)입니다. 유튜버(주체)는 구독자(옵저버)가 몇 명인지, 누구인지 알 필요 없이 그저 "알림 목록"에 신호를 보낼 뿐이고, 구독자 역시 유튜버와 직접 연결된 것이 아니라 '채널'이라는 주제(Topic)를 통해 소식만 전달받습니다.


🏫 클래식 구현 (Java 예시)

이 비유는 아래의 고전적인 Java 구현 방식과 정확히 일치합니다.

import java.util.ArrayList;
import java.util.List;

// 1. 주체 인터페이스 (채널의 기능 명세)
interface Subject {
  public void register(Observer obj);    // 구독 (리스트 추가)
  public void unregister(Observer obj);  // 구독 취소 (리스트 제거)
  public void notifyObservers();       // 모든 구독자에게 알림 발송
  public Object getUpdate(Observer obj); // 새 소식(영상) 가져오기
}

// 2. 옵저버 인터페이스 (구독자의 기능 명세)
interface Observer {
  public void update(); // 알림 받기
}

// 3. 실제 주체 (실제 유튜브 채널)
class Topic implements Subject {
  private List<Observer> observers; // 구독자 리스트
  private String message;           // 새 영상(소식)

  public Topic() {
    this.observers = new ArrayList<>();
    this.message = "";
  }

  @Override
  public void register(Observer obj) {
    if (!observers.contains(obj)) observers.add(obj);
  }

  @Override
  public void unregister(Observer obj) {
    observers.remove(obj);
  }

  // 4. 모든 구독자에게 "update() 해!"라고 신호를 보냄
  @Override
  public void notifyObservers() {
    this.observers.forEach(Observer::update);
  }

  @Override
  public Object getUpdate(Observer obj) {
    return this.message;
  }

  // 5. 새 영상(소식)이 올라옴 (상태 변화 발생)
  public void postMessage(String msg) {
    System.out.println("Message sended to Topic: " + msg);
    this.message = msg;
    notifyObservers(); // 즉시 모든 구독자에게 알림
  }
}

// 6. 실제 옵저버 (실제 구독자)
class TopicSubscriber implements Observer {
  private String name;    // 구독자 이름
  private Subject topic; // 구독 중인 채널

  public TopicSubscriber(String name, Subject topic) {
    this.name = name;
    this.topic = topic;
  }

  // 7. 알림(update)을 받으면, 채널(topic)에 가서 새 소식(getUpdate)을 가져와 출력
  @Override
  public void update() {
    String msg = (String) topic.getUpdate(this);
    System.out.println(name + ":: got message>>" + msg);
  }
}

// --- 실행 ---
public class HelloWorld {
  public static void main(String[] args) {
    Topic topic = new Topic(); // 채널 개설

    // 구독자 생성
    Observer a = new TopicSubscriber("a", topic);
    Observer b = new TopicSubscriber("b", topic);
    Observer c = new TopicSubscriber("c", topic);

    // 구독 신청
    topic.register(a);
    topic.register(b);
    topic.register(c);

    // 새 영상 업로드! (상태 변경 -> 자동 알림)
    topic.postMessage("amumu is op champion!!");
  }
}

실행 결과:

Message sended to Topic: amumu is op champion!!
a:: got message>>amumu is op champion!!
b:: got message>>amumu is op champion!!
c:: got message>>amumu is op champion!!

👥 자바: 상속(extends) vs 구현(implements)

Java 예제를 이해하려면 두 키워드의 차이를 알아야 합니다.

  • 상속 (extends): 자식 클래스가 부모 클래스의 메서드 등을 물려받아 사용하며, 기능을 추가 및 확장(override)할 수 있습니다. (클래스, 추상 클래스 대상)
    • 핵심: 코드 재사용성 및 중복 최소화. "A는 B이다." (예: Dog extends Animal)
  • 구현 (implements): 부모 인터페이스(껍데기/계약서)를 자식 클래스에서 반드시 재정의(구현)해야 합니다. (인터페이스 대상)
    • 핵심: 역할과 규칙 강제. "A는 B를 할 수 있다." (예: Dog implements Barkable)

⚙️ 모던 구현 (JavaScript와 Proxy 객체)

Java가 인터페이스(implements)를 통해 '구독'과 '알림'을 명시적으로 구현했다면, 최신 자바스크립트는 언어 자체의 강력한 기능을 사용해 옵저버 패턴을 훨씬 간결하게 구현합니다. 그 핵심 도구가 바로 Proxy 객체입니다.

프록시 (Proxy) 객체란?

어떠한 대상 객체의 기본적인 동작(속성 접근, 할당, 순회, 함수 호출 등)을 중간에서 가로챌 수 있는(Intercept) 객체입니다.

 

Proxy는 두 개의 매개변수를 가집니다.

  1. target: 프록시(대리인)가 감시할 원본 대상 객체.
  2. handler: target의 동작을 가로챘을 때(예: 값을 읽거나, 쓰거나), 어떤 추가 동작(Side Effect)을 할 것인지 설정해 둔 함수들의 집합.

[간단한 Proxy 예제: get 가로채기]

const handler = {
  // target의 속성을 읽으려(get) 할 때 이 함수가 대신 실행됨
  get: function (target, name) {
    // 만약 'name' 속성을 읽으려고 하면, a와 b를 조합해서 가짜 값을 리턴
    return name === 'name' ? `${target.a} ${target.b}` : target[name];
  }
};

const p = new Proxy({ a: 'KUNDOL', b: 'IS AMUMU ZANGIN' }, handler);

// p.name을 읽으려고 하자, handler의 get이 작동하여 조합된 문자열을 반환
console.log(p.name); // KUNDOL IS AMUMU ZANGIN

⛓️ 프록시 객체를 이용한 옵저버 패턴 (JS)

Java 예제에서는 postMessage()라는 '알림용 메서드'를 직접 호출해야 했습니다.

하지만 Proxy를 사용하면, 데이터 할당(set) 자체를 가로채서 그 순간에 콜백(알림)을 실행할 수 있습니다. notifyObservers()를 수동으로 호출할 필요가 없어집니다.

// target 객체와 callback(알림 함수)을 받아 Proxy 객체를 생성하는 공장 함수
function createReactiveObject(target, callback) {
  const proxy = new Proxy(target, {
    
    // 1. target의 속성에 값을 할당하려(set) 할 때 이 함수가 가로챔
    set(obj, prop, value) {
      
      // 2. 만약 값이 진짜로 변경되었을 때만 (불필요한 알림 방지)
      if (value !== obj[prop]) {
        const prev = obj[prop]; // 이전 값
        obj[prop] = value;       // 3. 실제 값을 변경
        
        // 4. 저장해둔 콜백(옵저버)을 즉시 실행하여 "변경"을 알림
        callback(`${prop}가 [${prev}] >> [${value}]로 변경되었습니다.`);
      }
      return true;
    }
  });
  return proxy;
}

const a = {
  "형규": "솔로"
};

// 5. a 객체를 감시(proxy)하고, 변경되면 console.log를 실행하는 b 객체 생성
const b = createReactiveObject(a, console.log);

// 값을 할당하지 않았으므로(기존값과 동일) 아무 일도 일어나지 않음
b.형규 = "솔로"; 

// 6. 값을 할당 (set) -> Proxy의 set 핸들러가 작동 -> 콜백(console.log) 실행!
b.형규 = "커플"; 

실행 결과:

형규가 [솔로] >> [커플]로 변경되었습니다.

⚖️ Vue.js 3.0의 옵저버 패턴

이러한 JavaScript의 Proxy를 활용한 옵저버 패턴이 가장 활발하게 사용되는 곳이 바로 Vue.js와 같은 최신 프론트엔드 프레임워크입니다.

Vue에서 ref나 reactive로 데이터를 정의하면, Vue는 이 데이터를 즉시 Proxy 객체로 감싸버립니다.

  • DOM (Document Object Model): 웹 브라우저 화면을 이루는 요소들(버튼, 텍스트 등).
  • 작동: 개발자가 b.형규 = "커플"처럼 데이터만 변경하면, Proxy의 set 핸들러가 가로채서 이 변경을 감지합니다. 그리고 이 데이터(주체)를 구독(관찰)하고 있던 DOM(옵저버)에게 update() 알림을 보내 화면을 자동으로 다시 그리게 됩니다.

아래는 실제 Vue 3.0의 반응형 객체 생성 소스 코드의 일부이며, 우리가 만든 createReactiveObject 함수처럼 Proxy를 사용하여 객체를 생성하는 것을 볼 수 있습니다. (훨씬 더 복잡하고 많은 예외 처리가 포함되어 있습니다.)

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`);
    }
    return target;
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target;
  }
  // target already has corresponding Proxy
  const existingProxy = proxyMap.get(target);
  if (existingProxy) {
    return existingProxy;
  }
  // only a whitelist of value types can be observed.
  const targetType = getTargetType(target);
  if (targetType === TargetType.INVALID) {
    return target;
  }
  // --- 핵심: Proxy 생성 ---
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  );
  proxyMap.set(target, proxy);
  return proxy;
}

 

 


1. 옵저퍼 패턴을 어떻게 구현하나요?

 

옵저버 패턴은 주체와 관찰자라는 두 가지 핵심 역할로 구현합니다. 먼저, 주체 객체는 자신을 관찰할 관찰자들을 등록하고 제거하는 메서드와, 상태가 변했을 때 이들에게 notify. 즉, 알림을 보내는 메서드를 가집니다. 관찰자는 주체로부터 알림을 받을 update 메서드를 가지고 있습니다. 그래서 주체의 상태가 변하면, nofity 메서드가 등록된 모든 관찰자의 update 메서드를 호출하여 변경 사항을 전파하는 방식으로 동작합니다.

 

2. 프록시를 사용해서 해결할 수 있는 구체적인 문제는 무엇이 있을까요?

 

프록시 객체를 사용하면 기존 객체의 코드를 직접 수정하지 않고도 다양한 부가 기능을 추가하여 문제를 해결할 수 있습니다.가장 대표적으로, 객체의 속성에 값을 할당할 때 데이터 유효성을 검사하거나, 특정 메서드가 호출될 때마다 기록을 남기는 로깅 기능을 구현할 수 있습니다.

'Computer Science > Design Pattern' 카테고리의 다른 글

🔁 이터레이터 패턴  (0) 2025.09.16
⛓️ 프록시 패턴과 프록시 서버  (1) 2025.09.16
⚔️ 전략 패턴 (Strategy Pattern)  (0) 2025.09.09
🏭 팩토리 패턴 (Factory Pattern)  (0) 2025.09.09
💡 싱글톤 패턴 (Singleton Pattern)  (0) 2025.09.09
'Computer Science/Design Pattern' 카테고리의 다른 글
  • 🔁 이터레이터 패턴
  • ⛓️ 프록시 패턴과 프록시 서버
  • ⚔️ 전략 패턴 (Strategy Pattern)
  • 🏭 팩토리 패턴 (Factory Pattern)
TECHNING
TECHNING
Hi! I'm techning
  • TECHNING
    TECHNING
    TECHNING
    • 분류 전체보기 (54)
      • Computer Science (45)
        • Design Pattern (11)
        • Programming Paradigm (4)
        • Network (15)
        • Operating System (6)
        • Database (6)
        • Data Structure (3)
      • Algorithm (5)
        • Python (3)
        • Java (1)
      • IT Insight (4)
  • hELLO· Designed By정상우.v4.10.4
TECHNING
📡 옵저버 패턴 (Observer Pattern)
상단으로

티스토리툴바