[Design Pattern] 옵저버 패턴

olwooz·2023년 2월 28일
0

Design Pattern

목록 보기
18/22
여러 객체들에게 관찰 중인 객체에 발생하는 이벤트들에 대해 알려주는 
구독 메커니즘을 정의할 수 있게 해주는 행동 패턴

문제

고객, 매장 두 종류의 객체가 있음

  • 고객 - 매장에 곧 들어올 특정 브랜드의 제품에 관심이 있음 → 매일 매장에 방문해 재고를 확인할 수 있지만 제품이 들어오기 전까진 무의미함
  • 매장 - 새 제품이 들어올 때 모든 고객들에게 이메일 보냄 → 몇몇 고객들은 좋지만 다른 고객들에게는 스팸
  • 고객들이 제품 재고를 확인하느라 시간을 낭비하거나, 매장이 잘못된 고객들에게 알림을 보내 자원을 낭비하거나

해결책

중요한 상태를 가지는 객체를 종종 ‘주제 subject’라고 부르지만, 다른 객체들에게 자신의 상태 변경을 알리기도 하기 때문에 ‘발행자 publisher'라고 부름

발행자의 상태를 추적하려는 다른 객체들은 ‘구독자’ 라고 부름

옵저버 패턴 - 발행자 클래스에 구독 메커니즘을 추가해 각 객체들이 발행자로부터 오는 이벤트들을 구독/구독 취소 할 수 있음

  • 메커니즘은 구독자 객체들에 대한 참조를 저장하는 배열 필드와, 배열에 구독자들을 추가/제거하는 public 메서드 보유

발행자에게 중요한 이벤트가 발생하면 구독자들에게 특정 알림 메서드를 호출

실제 앱은 여러 종류의 구독자 클래스들이 동일한 발행자 클래스의 이벤트들을 추적할 수 있음

  • 발행자들을 모든 클래스들에게 결합시키지 않기 위해 모든 구독자들이 같은 인터페이스를 구현해 발행자와 오직 그 인터페이스를 통해서만 소통
  • 인터페이스는 알림 메서드와 컨텍스트 데이터를 전달할 매개변수 집합 선언

앱이 여러 종류의 발행자가 있고 구독자들이 모든 발행자와 호환되게 하려면 모든 발행자들이 같은 인터페이스를 따르도록 하면 됨

  • 인터페이스는 몇 개의 구독 메서드들만 설명
  • 구독자들이 발행자의 concrete 클래스들에 결합하지 않고 상태를 관찰할 수 있게 함

구조

1. 발행자 - 다른 객체들이 관심 가지는 이벤트를 발행
   - 이벤트는 발행자가 자신의 상태를 바꾸거나 몇몇 행동을 하면 발생
   - 발행자는 구독자 추가/제거가 가능한 구독 인프라 포함
   
2. 이벤트가 발생하면 발행자가 각 구독자에게 구독자 인터페이스에 선언된 알림 메서드 호출

3. 구독자 - 알림 인터페이스를 선언하는 인터페이스, 대개 단일 `update` 메서드로 구성
   - 메서드는 발행자가 업데이트와 이벤트 세부 사항을 전달할 수 있는 여러 매개변수를 가질 수 있음
    
4. concrete 구독자 - 발행자가 발행한 알림에 대한 응답으로 몇몇 작업 실행
   - 모든 concrete 구독자 클래스들은 같은 인터페이스를 구현해 
     발행자가 concrete 클래스들에 결합되지 않게 해야 함
   
5. 대개 구독자들은 업데이트를 올바르게 다루기 위해 컨텍스트 정보 필요 
   → 발행자들은 종종 알림 메서드의 인수로 컨텍스트 데이터 전달
   - 발행자는 스스로를 인수로 전달해 구독자가 직접 필요한 데이터를 가져오게 할 수 있음
   
6. 클라이언트 - 발행자와 구독자를 별도로 생성하고, 구독자를 발행자 업데이트에 등록

적용

한 객체의 상태가 변화되면 다른 객체들도 변경해야 하고, 변경되는 객체들의 집합이 미리 알려지지 않았거나 동적으로 바뀌는 경우

- GUI 클래스들을 다룰 때 종종 발생하는 문제 
  (e.g. 커스텀 버튼 클래스를 정의하고 클라이언트가 버튼에 custom code를 연결해 
   버튼을 누를 때마다 실행시키고 싶은 경우)
- 옵저버 패턴 - 구독자 인터페이스를 구현한 모든 객체들이 발행자 객체의 이벤트 알림 구독 가능 
  → 버튼에 구독 메커니즘 추가, 클라이언트가 custom 구독자 클래스를 통해 custom code 연결

앱 내부 객체들이 한정된 시간 내에 또는 특정 경우에만 다른 객체들을 관찰해야 하는 경우

- 구독 리스트는 동적이기 때문에 구독자들이 필요할 때 리스트에 참여/탈퇴 가능

구현방법

1. 비즈니스 로직을 두 부분으로 분할 - 다른 코드와 독립된 핵심 기능은 발행자 역할 수행, 나머지는 구독자 클래스들

2. 구독자 인터페이스 선언, 최소한 하나의 `update` 메서드는 선언해야 함

3. 발행자 인터페이스를 선언하고 리스트에 구독자 객체 추가/제거하는 메서드들 설명
   - 발행자는 구독자 인터페이스를 통해서만 구독자와 협업 가능
    
4. 실제 구독 리스트와 구독 메서드 구현을 어디에 위치시킬 지 결정
   - 대개 이 코드는 모든 유형의 발행자에게 동일하기 때문에 발행자 인터페이스로부터 직접 파생된 추상 클래스에 위치
   - concrete 발행자는 해당 클래스를 확장해 구독 행동 상속
   - 패턴을 기존 클래스 계층에 적용하는 상황이라면 합성 기반 접근 방식 고려: 
     구독 로직을 별도 객체에 넣고 실제 발행자들이 해당 객체를 사용하게 함
   
5. concrete 발행자 클래스 생성, 발행자 내부에서 중요한 일이 발생하면 구독자에게 알려야 함

6. concrete 구독자 클래스에 update 알림 메서드 구현
   - 대부분 구독자는 이벤트에 대한 컨텍스트 데이터 필요 → 알림 메서드의 인수로 전달될 수 있음
   - 대안 - 발행자가 update 메서드 인수로 자기 자신을 전달, 알림을 받으면 구독자가 직접 필요한 데이터 가져옴
   
7. 클라이언트가 필요한 구독자들을 모두 생성해 적절한 발행자에 등록해야 함

장단점

장점

- OCP - 발행자 코드를 변경하지 않고 새로운 구독자 추가 가능 (발행자 인터페이스가 있으면 그 반대도 가능)
- 런타임에 객체 간 관계 수립 가능

단점

- 구독자들은 무작위 순서로 알림을 받음

다른 패턴과의 관계

- 책임 연쇄, 커맨드, 중재자, 옵저버 패턴은 요청의 발신자와 수신자를 연결하는 다양한 방법을 다룸
  - 책임 연쇄 패턴 - 요청이 처리될 때까지 잠재적 수신자들로 구성된 동적 체인을 따라 전달함
  - 커맨드 - 수신자와 발신자 단방향 커넥션 수립
  - 중재자 - 수신자와 발신자 사이의 직접적인 연결을 제거하고 중재자 객체를 통해서만 소통하게 함
  - 옵저버 - 수신자들이 동적으로 요청 수신을 구독/구독 취소할 수 있음
    
- 중재자와 옵저버의 차이가 애매함
  - 대부분 둘 중 한 패턴을 구현하지만 때로는 둘 다 동시에 적용할 수 있음
  - 중재자의 목표 - 시스템 컴포넌트들의 집합 간 상호 의존성을 없애는 것, 컴포넌트들은 단일 중재자 객체에 의존
  - 옵저버의 목표 - 객체들 간에 동적 단방향 커넥션 수립, 일부 객체는 다른 객체의 종속자 역할 수행
  - 옵저버에 의존하는 중재자 패턴의 인기 있는 구현 존재 - 중재자 객체는 발행자, 
    컴포넌트들은 중재자 이벤트를 구독/구독 취소하는 구독자 역할을 함 → 옵저버와 비슷
  - 중재자 패턴은 다른 방식들로 구현 가능 
    e.g. 모든 컴포넌트들을 같은 중재자 객체에 영구적으로 연결할 수 있음 
       → 옵저버와 닮지 않았지만 여전히 중재자 패턴
  - 모든 컴포넌트들이 발행자가 돼 서로 동적 연결을 허용하면 
    중앙화된 중재자 객체는 없고 옵저버의 분산된 집합만 존재하게 됨

TypeScript 예제

/**
 * The Subject interface declares a set of methods for managing subscribers.
 */
interface Subject {
    // Attach an observer to the subject.
    attach(observer: Observer): void;

    // Detach an observer from the subject.
    detach(observer: Observer): void;

    // Notify all observers about an event.
    notify(): void;
}

/**
 * The Subject owns some important state and notifies observers when the state
 * changes.
 */
class ConcreteSubject implements Subject {
    /**
     * @type {number} For the sake of simplicity, the Subject's state, essential
     * to all subscribers, is stored in this variable.
     */
    public state: number;

    /**
     * @type {Observer[]} List of subscribers. In real life, the list of
     * subscribers can be stored more comprehensively (categorized by event
     * type, etc.).
     */
    private observers: Observer[] = [];

    /**
     * The subscription management methods.
     */
    public attach(observer: Observer): void {
        const isExist = this.observers.includes(observer);
        if (isExist) {
            return console.log('Subject: Observer has been attached already.');
        }

        console.log('Subject: Attached an observer.');
        this.observers.push(observer);
    }

    public detach(observer: Observer): void {
        const observerIndex = this.observers.indexOf(observer);
        if (observerIndex === -1) {
            return console.log('Subject: Nonexistent observer.');
        }

        this.observers.splice(observerIndex, 1);
        console.log('Subject: Detached an observer.');
    }

    /**
     * Trigger an update in each subscriber.
     */
    public notify(): void {
        console.log('Subject: Notifying observers...');
        for (const observer of this.observers) {
            observer.update(this);
        }
    }

    /**
     * Usually, the subscription logic is only a fraction of what a Subject can
     * really do. Subjects commonly hold some important business logic, that
     * triggers a notification method whenever something important is about to
     * happen (or after it).
     */
    public someBusinessLogic(): void {
        console.log('\nSubject: I\'m doing something important.');
        this.state = Math.floor(Math.random() * (10 + 1));

        console.log(`Subject: My state has just changed to: ${this.state}`);
        this.notify();
    }
}

/**
 * The Observer interface declares the update method, used by subjects.
 */
interface Observer {
    // Receive update from subject.
    update(subject: Subject): void;
}

/**
 * Concrete Observers react to the updates issued by the Subject they had been
 * attached to.
 */
class ConcreteObserverA implements Observer {
    public update(subject: Subject): void {
        if (subject instanceof ConcreteSubject && subject.state < 3) {
            console.log('ConcreteObserverA: Reacted to the event.');
        }
    }
}

class ConcreteObserverB implements Observer {
    public update(subject: Subject): void {
        if (subject instanceof ConcreteSubject && (subject.state === 0 || subject.state >= 2)) {
            console.log('ConcreteObserverB: Reacted to the event.');
        }
    }
}

/**
 * The client code.
 */

const subject = new ConcreteSubject();

const observer1 = new ConcreteObserverA();
subject.attach(observer1);

const observer2 = new ConcreteObserverB();
subject.attach(observer2);

subject.someBusinessLogic();
subject.someBusinessLogic();

subject.detach(observer2);

subject.someBusinessLogic();
// Output.txt

Subject: Attached an observer.
Subject: Attached an observer.

Subject: I'm doing something important.
Subject: My state has just changed to: 6
Subject: Notifying observers...
ConcreteObserverB: Reacted to the event.

Subject: I'm doing something important.
Subject: My state has just changed to: 1
Subject: Notifying observers...
ConcreteObserverA: Reacted to the event.
Subject: Detached an observer.

Subject: I'm doing something important.
Subject: My state has just changed to: 5
Subject: Notifying observers...

참고 자료: Refactoring.guru

0개의 댓글