[Design Pattern] Pub-Sub Pattern(발행-구독 패턴)

Main·2024년 8월 13일
0

Design Pattern

목록 보기
2/7

Pub-Sub Pattern?

Pub-Sub(발행-구독) 패턴은 소프트웨어 아키텍처에서 메시징을 통해 컴포넌트 간의 통신을 관리하는 디자인 패턴입니다. 이 패턴은 두 가지 주요 역할인 발행자(Publisher)와 구독자(Subscriber)로 구성됩니다.

Pub_Sub_Pattern


Pub-Sub 패턴의 구성요소

  • Publisher(발행자): 이벤트나 메시지를 생성하고 발행하는 역할을 합니다. 발행자는 메시지를 누구에게 보낼지 신경 쓰지 않고, 단지 메시지를 발행하면 됩니다. 발행자는 구독자가 누구인지 알 필요가 없습니다.
  • Subscriber(구독자): 특정 유형의 이벤트나 메시지를 수신하고, 그에 따라 동작을 수행합니다. 구독자는 발행자가 누구인지 알 필요가 없고, 특정 이벤트를 구독하기만 하면 됩니다.
  • Event Broker(이벤트 브로커) 또는 Message Broker(메시지 브로커): 발행자와 구독자 사이의 중개자 역할을 합니다. 발행자가 발행한 메시지를 받아서, 해당 메시지에 관심 있는 모든 구독자에게 전달합니다. 이 브로커가 중간에 존재함으로써 발행자와 구독자가 서로의 존재를 몰라도 되며, 시스템의 결합도를 낮출 수 있습니다.

Event Broker와 Message Broker 비교

Event Borker
이벤트 브로커는 주로 이벤트 기반 아키텍처에서 사용되며, 특정 이벤트가 발생했을 때 이를 감지하고 해당 이벤트를 구독한 컴포넌트에 전달합니다.

특징

  • 비동기성: 이벤트 발생 후 즉시 반응하지 않고, 구독자가 해당 이벤트를 수신할 때까지 대기합니다.
  • 상태 비저장: 이벤트 브로커는 이벤트의 상태를 저장하지 않으며, 이벤트가 발생할 때마다 실시간으로 전파합니다.
  • 이벤트 중심: 특정한 이벤트에 대한 처리를 중점적으로 다루며, 이벤트의 생명 주기에 따라 처리됩니다.

사용 예시

  • 사용자 인터페이스(UI)에서 버튼 클릭과 같은 이벤트 처리
  • 시스템 내의 상태 변화 알림

Message Broker

메시지 브로커는 메시지를 전송하고 수신하는 역할을 하며, 발행자와 구독자 간의 데이터 전송을 중개합니다. 메시지의 큐잉 및 라우팅을 포함한 복잡한 메시징 패턴을 지원합니다.

특징

  • 비동기 통신: 메시지가 발행된 후 수신자가 즉시 처리하지 않아도 됩니다. 메시지는 큐에 저장될 수 있습니다.
  • 상태 저장: 메시지 브로커는 메시지를 저장하고, 필요할 때 구독자에게 전달할 수 있습니다. 이는 메시지 손실을 방지하는 데 유용합니다.
  • 다양한 프로토콜 지원: 여러 프로토콜을 사용해 서로 다른 시스템 간의 통신을 지원합니다.

사용 예시

  • 대규모 시스템 간의 데이터 전송
  • 비즈니스 프로세스의 통합
구분이벤트 브로커메시지 브로커
주요 기능이벤트 감지 및 전달메시지 전송, 큐잉, 라우팅
상태 관리상태 비저장상태 저장
비동기 처리이벤트 발생 시 즉시 반응메시지 큐에 저장 후 수신 처리 가능
사용 예UI 이벤트 처리, 상태 변화 알림시스템 간 데이터 전송, 비즈니스 통합

Pub-Sub 패턴의 작동 방식

1 ) 발행(Publish)

발행자는 특정 이벤트가 발생하면, 그 이벤트와 관련된 데이터를 메시지로 만들어 브로커에게 전달합니다. 이 메시지에는 이벤트의 타입이나 메시지의 내용이 포함됩니다.

2 ) 구독(Subscribe)

구독자는 자신이 관심 있는 특정 이벤트나 메시지 타입을 브로커에게 알려주고, 이를 구독합니다. 구독자는 특정 이벤트가 발행될 때 이를 수신할 준비를 합니다.

3 ) 중개(Distribute)

브로커는 발행자로부터 메시지를 수신하면, 그 메시지를 해당 이벤트를 구독한 모든 구독자에게 전달합니다. 이 과정에서 브로커는 메시지를 필터링하거나, 특정 조건에 맞는 구독자에게만 메시지를 전달할 수 있습니다.

4 )수신(Receive)

구독자는 브로커로부터 메시지를 수신하고, 이 메시지에 따라 필요한 동작을 수행합니다. 예를 들어, UI를 업데이트하거나, 데이터베이스를 수정하는 등의 작업을 할 수 있습니다.


Pub-Sub 패턴의 장점

  • 저결합성: 발행자와 구독자는 서로를 전혀 알 필요가 없기 때문에 시스템의 결합도가 낮아집니다. 이는 시스템의 유지보수가 용이해지고, 확장성이 높아지는 결과를 가져옵니다.
  • 확장성: 새로운 구독자를 추가하거나 기존 구독자를 제거할 때 발행자를 수정할 필요가 없습니다. 시스템이 커져도 동일한 패턴으로 확장할 수 있습니다.
  • 유연성: 메시지 브로커를 통해 메시지를 필터링하거나 변환하여 구독자에게 전달할 수 있어, 시스템 설계의 유연성을 제공합니다.
  • 비동기성: 발행자와 구독자가 독립적으로 작동할 수 있어 비동기 처리에 적합합니다. 발행자는 메시지를 발행하고 즉시 다른 작업을 수행할 수 있으며, 구독자는 자신의 속도에 맞게 메시지를 처리할 수 있습니다.

Pub-Sub 패턴의 단점

  • 디버깅의 어려움: 발행자와 구독자가 서로 직접 연결되지 않기 때문에 메시지의 흐름을 추적하거나 디버깅하는 것이 어려울 수 있습니다.
  • 성능 문제: 많은 메시지 트래픽이 발생하면 브로커에 부하가 걸릴 수 있으며, 이는 성능 저하로 이어질 수 있습니다.
  • 메시지 유실: 네트워크나 시스템 장애로 인해 메시지가 유실될 가능성이 있습니다. 이를 방지하기 위해 메시지 큐를 사용하는 등의 방법이 필요할 수 있습니다.

Pub-Sub 패턴 구현하기

EventBroker.ts

type EventCallback = (data: any) => void;

export class EventBroker {
     // 이벤트 저장소: 이벤트 이름을 키로 하고, 해당 이벤트의 콜백 함수 배열을 값으로 가집니다.
    private events: { [key: string]: EventCallback[] } = {};
    
     // 구독: 지정된 이벤트에 대해 콜백 함수를 등록합니다.
    subscribe(event: string, callback: EventCallback): void {
        // 이벤트가 아직 등록되지 않은 경우, 빈 배열로 초기화
        if (!this.events[event]) {
            this.events[event] = [];
        }
        // 콜백 함수를 해당 이벤트의 콜백 배열에 추가
        this.events[event].push(callback);
    }
    
    // 구독 해제: 지정된 이벤트에서 특정 콜백 함수를 제거합니다.
    unsubscribe(event: string, callback: EventCallback): void {
        // 해당 이벤트가 등록되어 있지 않은 경우, 아무 것도 하지 않음
        if (!this.events[event]) return;
        
        // 콜백 배열에서 특정 콜백 함수를 필터링하여 제거
        this.events[event] = this.events[event].filter(cb => cb !== callback);
    }

    // 발행: 지정된 이벤트에 대해 모든 등록된 콜백 함수를 호출합니다.
    publish(event: string, data: any): void {
        // 해당 이벤트에 등록된 콜백이 없는 경우, 아무 것도 하지 않음
        if (!this.events[event]) return;

        // 모든 콜백 함수에 데이터를 전달하여 실행
        this.events[event].forEach(callback => callback(data));
    }
}

Publisher.ts

import { EventBroker } from './EventBroker';

export class Publisher {
  // EventBroker 인스턴스를 받아 초기화합니다.
  constructor(private eventBroker: EventBroker) {}

  // 이벤트를 발행합니다.
  publish(event: string, data: any): void {
      // EventBroker의 publish 메서드를 호출하여 이벤트를 발행
      this.eventBroker.publish(event, data);
  }
}

Subscriber.ts

import { EventBroker } from './EventBroker';

export class Subscriber {
  // EventBroker 인스턴스를 받아 초기화합니다.
  constructor(private eventBroker: EventBroker) {}
  
  // 지정된 이벤트를 구독합니다.
  subscribeToEvent(event: string, callback: (data: any) => void): void {
      // EventBroker의 subscribe 메서드를 호출하여 이벤트를 구독
      this.eventBroker.subscribe(event, callback);
  }
  
  // 지정된 이벤트의 구독을 해제합니다.
  unsubscribeFromEvent(event: string, callback: (data: any) => void): void {
      // EventBroker의 unsubscribe 메서드를 호출하여 이벤트의 구독 해제
      this.eventBroker.unsubscribe(event, callback);
  }
}

index.ts

import { EventBroker } from './src/EventBroker';
import { Publisher } from './src/Publisher';
import { Subscriber } from './src/Subscriber';

const eventBroker = new EventBroker();

const publisher = new Publisher(eventBroker);
const subscriber1 = new Subscriber(eventBroker);
const subscriber2 = new Subscriber(eventBroker);

const onMessageReceived1 = (data: any) => {
    console.log(`Subscriber 1 received: ${data}`);
};

const onMessageReceived2 = (data: any) => {
    console.log(`Subscriber 2 received: ${data}`);
};

// 구독자 1과 2가 "message" 이벤트에 구독
subscriber1.subscribeToEvent("message", onMessageReceived1);
subscriber2.subscribeToEvent("message", onMessageReceived2);

// Publisher가 메시지 발행
publisher.publish("message", "Hello, Subscribers!");

// 구독자 1이 구독 해제
subscriber1.unsubscribeFromEvent("message", onMessageReceived1);

// 다시 메시지 발행
publisher.publish("message", "Hello again!");

Pub-Sub 패턴으로 TodoList 구현하기

TodoList.ts

import { EventBroker } from "./EventBroker";
import { Publisher } from "./Publisher";
// Publiser 역할
// TodoList 클래스는 작업(Task)을 관리하고, 작업의 추가 및 제거 시 이벤트를 발행합니다.
export class TodoList extends Publisher {
  tasks: string[];
  constructor(eventBroker: EventBroker) {
    // 부모 클래스인 Publisher를 초기화합니다.
    super(eventBroker);
    this.tasks = [];
  }

  addTask(task: string) {
    this.tasks.push(task);
    // 작업 추가 후 현재 작업 목록을 발행
    this.publish("task", this.tasks);
  }

  removeTask(task: string) {
    this.tasks = this.tasks.filter((t) => t !== task); 
    // 작업 제거 후 현재 작업 목록을 발행
    this.publish("task", this.tasks);
  }
}

RenderUI.ts

import { EventBroker } from "./EventBroker";
import { Subscriber } from "./Subscriber";
import { TodoList } from "./TodoList";

// Subscriber의 역할
// RenderUI 클래스는 TodoList의 작업 목록을 UI에 렌더링하는 역할을 합니다.
export class RenderUI extends Subscriber {
  todoList: TodoList;

  constructor(eventBroker: EventBroker, todoList: TodoList) {
    // 부모 클래스인 Subscriber를 초기화합니다.
    super(eventBroker);
    this.todoList = todoList;
    
    // "task" 이벤트를 구독하고, 해당 이벤트 발생 시 render 메서드를 호출합니다.
    this.subscribeToEvent("task", (taskList) => this.render(taskList));
  }

  // TodoList를 UI에 렌더링합니다.
  render(taskList: string[]) {
    const taskListElement = document.getElementById("taskList");
    taskListElement!.innerHTML = "";

    taskList.forEach((task) => {
      const li = document.createElement("li");
      li.textContent = task;

      const removeButton = document.createElement("button");
      removeButton.textContent = "제거";
      removeButton.addEventListener("click", () => {
        this.todoList.removeTask(task);
      });

      li.appendChild(removeButton);
      taskListElement!.appendChild(li);
    });

    const taskCountElement = document.getElementById("taskCount");
    taskCountElement!.textContent = taskList.length.toString();
  }
}

index.ts

import { EventBroker } from "./src/EventBroker";
import { RenderUI } from "./src/RenderUI";
import { TodoList } from "./src/TodoList";
import "./styles.css";

const eventBroker = new EventBroker();

const todoList = new TodoList(eventBroker);

new RenderUI(eventBroker, todoList);

document.getElementById("addTaskButton")!.addEventListener("click", () => {
  const taskInput = document.getElementById("taskInput") as HTMLInputElement;
  const task = taskInput.value.trim();
  if (task) {
    todoList.addTask(task);
    taskInput.value = "";
  }
});

구현한 TodoList의 동작과정

1 ) 이벤트 브로커 생성

  • EventBroker 클래스는 이벤트를 관리합니다. 이벤트를 구독하고 발행하는 기능을 제공합니다.

2 ) TodoList 생성

  • TodoList 클래스는 작업의 추가 및 제거를 담당합니다. 이 클래스는 Publisher 클래스를 상속받아, 작업이 추가되거나 제거될 때마다 해당 이벤트를 발행합니다.
  • addTask(task: string) 메서드는 새로운 작업을 추가하고, 작업 목록을 발행합니다.
  • removeTask(task: string) 메서드는 작업을 제거하고, 업데이트된 작업 목록을 발행합니다.

3 ) RenderUI 생성

  • RenderUI 클래스는 Subscriber 클래스를 상속받아, 이벤트를 구독하여 UI를 업데이트합니다.
  • 생성자에서 TodoList 인스턴스를 받아, "task" 이벤트를 구독합니다. 이 이벤트가 발생하면 render 메서드를 호출하여 UI를 업데이트합니다.

4 ) UI 렌더링

  • render(taskList: string[]) 메서드는 UI를 업데이트합니다. 작업 목록을 HTML 요소에 렌더링하고, 각 작업마다 "제거" 버튼을 생성합니다.
  • "제거" 버튼을 클릭하면 해당 작업이 TodoList에서 제거됩니다.

5 ) 이벤트 흐름

  • 사용자가 작업을 추가하면 TodoList의 addTask 메서드가 호출됩니다. 이 메서드는 작업을 추가하고, 현재 작업 목록을 "task" 이벤트로 발행합니다.
  • RenderUI는 "task" 이벤트를 구독하고 있으므로, 이 이벤트가 발생하면 render 메서드가 호출되어 UI가 업데이트됩니다.
  • 사용자가 작업을 제거하면 TodoList의 removeTask 메서드가 호출되고, 작업 목록이 업데이트된 후 다시 "task" 이벤트가 발행되어 UI가 다시 렌더링됩니다.

정리

EventBroker: 이벤트를 관리하고, 구독 및 발행 기능 제공합니다.
Publisher: 이벤트를 발행하는 기능을 가진 클래스, TodoList에서 사용됩니다.
Subscriber: 이벤트를 구독하는 기능을 가진 클래스, RenderUI에서 사용됩니다.
TodoList: 작업을 관리하고, 추가 및 제거 시 이벤트를 발행합니다.
RenderUI: UI를 업데이트하며, TodoList의 작업 목록 변화에 반응합니다.

아래의 이미지와 같은 형태로 동작한다고 볼 수 있습니다.
Pub_Sub_Pattern_TodoList


정리

Pub-Sub 패턴은 소프트웨어 디자인 패턴 중 하나로, 객체 간의 느슨한 결합을 통해 통신을 관리하는 방법입니다. 이 패턴에서는 Publisher가 이벤트를 발행하고, SubScriber가 특정 이벤트를 구독하여 해당 이벤트가 발생할 때 알림을 받습니다. Publisher와 Subscriber 간의 직접적인 연결이 없기 때문에, 시스템의 유연성과 확장성이 높아지고, 새로운 기능을 추가하거나 수정할 때 다른 구성 요소에 미치는 영향을 줄일 수 있습니다. 이 패턴은 주로 이벤트 기반 아키텍처나 메시지 전송 시스템에서 사용됩니다.


참고 사이트

https://gobae.tistory.com/121

profile
함께 개선하는 프론트엔드 개발자

0개의 댓글