[Design Pattern] Observer Pattern(옵저버 패턴)

Main·2024년 8월 13일
0

Design Pattern

목록 보기
1/7

Observer Pattern?

옵저버 패턴(Observer Pattern)은 객체 간의 일대다(one-to-many) 의존 관계를 정의하여, 한 객체의 상태가 변경될 때 그 객체에 의존하는 모든 객체에 자동으로 알림을 주는 디자인 패턴입니다. 주로 이벤트 처리 시스템에서 사용되며, 느슨한 결합(Loose Coupling)을 유지할 수 있는 장점이 있습니다.


Observer Pattern의 구성요소

1 ) Observer(관찰자)

  • 상태 변화를 감지하고 이에 반응하는 객체입니다.
  • 일반적으로 update 메서드를 통해 Observable로부터 알림을 받습니다.

2 ) Observable(관찰대상)

  • 상태를 관리하고, 상태 변화가 있을 때 Observer에게 알림을 보내는 역할을 합니다.
  • Observer를 추가하거나 제거하는 메서드를 제공합니다.
  • 상태 변화가 발생했을 때 모든 Observer에게 알림을 전달하는 메서드를 포함합니다.

Observer Pattern의 장점과 단점

장점

  • 객체 간의 느슨한 결합(Loose Coupling)
    옵저버 패턴은 주체(Observable)와 옵저버(Observer) 간의 결합도를 낮춥니다. Observable 객체는 자신이 어떤 옵저버들에게 알림을 보내는지 알 필요가 없고, 옵저버도 Observable이 어떻게 상태를 관리하는지 알 필요가 없습니다. 이렇게 서로 독립적인 관계를 유지할 수 있어, 코드의 유연성과 재사용성이 증가합니다.

  • 동적인 구독 관리
    새로운 옵저버를 쉽게 추가할 수 있고, 기존 옵저버를 제거할 수도 있습니다. 이를 통해 시스템이 동적으로 확장되거나 축소될 수 있습니다. 예를 들어, 새로운 기능이나 모듈을 추가할 때 옵저버로 등록하기만 하면 별다른 수정 없이 기능이 통합될 수 있습니다.

  • 변경 감지 및 자동화된 알림
    Observable 객체의 상태가 변경될 때 자동으로 관련 옵저버들에게 알림을 보냅니다. 이를 통해 상태 변경에 따른 동기화 작업이 자동으로 이루어지며, 개발자가 수동으로 업데이트 작업을 관리할 필요가 없습니다.

  • 유연한 설계
    여러 개의 옵저버가 하나의 Observable을 구독할 수 있고, 하나의 옵저버가 여러 개의 Observable을 구독할 수 있습니다. 이러한 유연한 구조는 복잡한 애플리케이션의 설계에도 적합합니다.

단점

  • 알림의 예측 어려움
    옵저버 패턴은 여러 옵저버들에게 알림을 보내는 구조이기 때문에, 알림이 도착하는 순서나 옵저버들이 어떻게 반응할지 예측하기 어려울 수 있습니다. 특히, 알림의 순서나 타이밍이 중요한 경우 문제가 발생할 수 있습니다.

  • 디버깅 어려움
    옵저버 패턴에서는 이벤트나 상태 변경이 비동기적으로 발생할 수 있습니다. 이는 시스템의 복잡성을 증가시키며, 디버깅을 어렵게 만들 수 있습니다. 특히, 다수의 옵저버들이 다양한 시점에서 알림을 받는다면 어떤 부분에서 오류가 발생했는지 추적하기 어려워집니다.

  • 성능 문제
    옵저버의 수가 많아질수록, 상태 변경에 따른 알림이 증가하게 되어 성능 문제가 발생할 수 있습니다. 특히, 알림이 자주 발생하는 시스템에서는 모든 옵저버에게 알림을 보내는 과정이 성능 병목을 일으킬 수 있습니다.

  • 메모리 누수(Memory Leak) 가능성
    옵저버를 구독에서 해지하지 않고 그대로 남겨둘 경우, 더 이상 필요 없는 객체가 계속 메모리를 차지하게 되어 메모리 누수가 발생할 수 있습니다. 이를 방지하려면, 구독 해지를 적절히 관리해야 합니다.


Observer Pattern의 필수사항

  • 구독 방법을 포함해야합니다.
  • 구독 리스트를 저장해야합니다.
  • 이벤트를 발생시킬 방법을 포함해야합니다.

Observer Pattern 구현해보기

Observer Pattern를 간단히 구현한 예시코드입니다.

아래 코드에서 NewsMachine은 Observable 역할을 하며, 상태(여기서는 뉴스)의 변경을 Observer들에게 알립니다. Observer들은 subscribe 메서드를 통해 Observable에 등록되며, 상태가 변경될 때마다 알림을 받습니다. Observer는 언제든지 unsubscribe 메서드를 통해 등록을 해제할 수 있으며, 이후에는 더 이상 알림을 받지 않습니다.

// 상태변경을 감지
class Observer {
  // observer 구별을 위해 name 추가
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  update(title: string, content: string) {
    console.log(`${this.name}, Title: ${title}, Content: ${content}`);
  }
}

// 상태가 변경되는 대상
class Observable {
  observers: Array<Observer> = [];

  subscribe(observer: Observer) {
    this.observers.push(observer);
  }

  unsubscribe(observer: Observer) {
    this.observers = this.observers.filter((obs) => obs !== observer);
  }

  notifySubscribers(title: string, content: string) {
    this.observers.forEach((obs) => obs.update(title, content));
  }
}

// NewsMachine Observable 생성
class NewsMachine extends Observable {
  private news: Array<{ title: string; content: string }> = [];

  addNews(title: string, content: string) {
    this.news.push({ title, content });
    this.notifySubscribers(title, content); // 뉴스 추가 시 알림
  }

  getNews() {
    return this.news;
  }
}

// 사용 예시
const newsMachine = new NewsMachine();
const observer1 = new Observer("Observer1");
const observer2 = new Observer("Observer2");

newsMachine.subscribe(observer1);
newsMachine.subscribe(observer2);

newsMachine.addNews('새로운 소식', '옵저버 패턴을 배우고 있습니다!');
newsMachine.unsubscribe(observer1);
newsMachine.addNews('새로운 소식', 'observer1이 구독을 취소하였습니다!');

Observer Pattern으로 TodoList 구현하기

Observer pattern를 활용하여 TodoList를 구현해보겠습니다.
Observable은 TodoList로 Observer는 UI로 설정하여 구현하겠습니다.

Observable.ts

import { Observer } from './Observer';

// Observable 클래스: 상태 관리 및 옵저버들에게 알림
export class Observable {
  observers: Observer[];

  constructor() {
    this.observers = [];
  }

  subscribe(observer: Observer) {
    this.observers.push(observer);
  }

  unsubscribe(observer: Observer) {
    this.observers = this.observers.filter((obs) => obs !== observer);
  }

  notifySubscribers(taskList: string[]) {
    this.observers.forEach((observer) => observer.update(taskList));
  }
}

Observer.ts

// Observer 클래스
// update는 자식 클래스에서 구현
export abstract class Observer {
  abstract update(taskList: string[]): void;
}

TodoList.ts (Observable)

import { Observable } from "./Observable";

// TodoList 클래스: Observable 역할
export class TodoList extends Observable {
  tasks: string[];

  constructor() {
    super();
    this.tasks = [];
  }

  addTask(task: string) {
    if (task) {
      this.tasks.push(task);
      this.notifySubscribers(this.tasks);
    }
  }

  removeTask(task: string) {
    this.tasks = this.tasks.filter((t) => t !== task);
    this.notifySubscribers(this.tasks);
  }
}

RenderUI.ts (Observer)

import { Observer } from "./Observer";
import { TodoList } from "./TodoList";

// UI Component : 할 일 목록 표시
export class RenderUI extends Observer {
  todoList: TodoList;

  constructor(todoList: TodoList) {
    super();
    this.todoList = todoList;
  }

  // UI 렌더링
  update(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 { RenderUI } from "./src/RenderUI";
import { TodoList } from "./src/TodoList";
import "./styles.css";

// TodoList (Observable) 인스턴스 생성
const todoList = new TodoList();

// RenderUI (Observer) 생성 및 구독
const renderUI = new RenderUI(todoList);
todoList.subscribe(renderUI);

// 할 일 추가 버튼 클릭 이벤트
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는 Observable의 역할을 하며, 할 일을 추가/삭제 하며, addTask(), RemoveTask() 호출시 notifySubscribers() 메서드를 호출하여 Observer의 update 메서드를 호출하게됩니다.

RenderUI는 Observer의 역할을 하며, 구독한 TodoList에서 notifySubscribers()메서드를 호출할 때 마다 UI을 렌더링 해주는 update() 메서드를 실행시킵니다.

아래 이미지는 위 내용을 나타내는 이미지입니다.


정리

옵저버 패턴은 객체 간의 일대다 의존 관계를 정의하여, 한 객체의 상태가 변경될 때 그 객체에 의존하는 모든 객체에 자동으로 알림을 주는 디자인 패턴입니다.


참고 사이트

https://gobae.tistory.com/121

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

0개의 댓글