옵저버 패턴(Observer Pattern)은 객체 간의 일대다(one-to-many) 의존 관계를 정의하여, 한 객체의 상태가 변경될 때 그 객체에 의존하는 모든 객체에 자동으로 알림을 주는 디자인 패턴입니다. 주로 이벤트 처리 시스템에서 사용되며, 느슨한 결합(Loose Coupling)을 유지할 수 있는 장점이 있습니다.
1 ) Observer(관찰자)
update
메서드를 통해 Observable로부터 알림을 받습니다.2 ) Observable(관찰대상)
장점
객체 간의 느슨한 결합(Loose Coupling)
옵저버 패턴은 주체(Observable)와 옵저버(Observer) 간의 결합도를 낮춥니다. Observable 객체는 자신이 어떤 옵저버들에게 알림을 보내는지 알 필요가 없고, 옵저버도 Observable이 어떻게 상태를 관리하는지 알 필요가 없습니다. 이렇게 서로 독립적인 관계를 유지할 수 있어, 코드의 유연성과 재사용성이 증가합니다.
동적인 구독 관리
새로운 옵저버를 쉽게 추가할 수 있고, 기존 옵저버를 제거할 수도 있습니다. 이를 통해 시스템이 동적으로 확장되거나 축소될 수 있습니다. 예를 들어, 새로운 기능이나 모듈을 추가할 때 옵저버로 등록하기만 하면 별다른 수정 없이 기능이 통합될 수 있습니다.
변경 감지 및 자동화된 알림
Observable 객체의 상태가 변경될 때 자동으로 관련 옵저버들에게 알림을 보냅니다. 이를 통해 상태 변경에 따른 동기화 작업이 자동으로 이루어지며, 개발자가 수동으로 업데이트 작업을 관리할 필요가 없습니다.
유연한 설계
여러 개의 옵저버가 하나의 Observable을 구독할 수 있고, 하나의 옵저버가 여러 개의 Observable을 구독할 수 있습니다. 이러한 유연한 구조는 복잡한 애플리케이션의 설계에도 적합합니다.
단점
알림의 예측 어려움
옵저버 패턴은 여러 옵저버들에게 알림을 보내는 구조이기 때문에, 알림이 도착하는 순서나 옵저버들이 어떻게 반응할지 예측하기 어려울 수 있습니다. 특히, 알림의 순서나 타이밍이 중요한 경우 문제가 발생할 수 있습니다.
디버깅 어려움
옵저버 패턴에서는 이벤트나 상태 변경이 비동기적으로 발생할 수 있습니다. 이는 시스템의 복잡성을 증가시키며, 디버깅을 어렵게 만들 수 있습니다. 특히, 다수의 옵저버들이 다양한 시점에서 알림을 받는다면 어떤 부분에서 오류가 발생했는지 추적하기 어려워집니다.
성능 문제
옵저버의 수가 많아질수록, 상태 변경에 따른 알림이 증가하게 되어 성능 문제가 발생할 수 있습니다. 특히, 알림이 자주 발생하는 시스템에서는 모든 옵저버에게 알림을 보내는 과정이 성능 병목을 일으킬 수 있습니다.
메모리 누수(Memory Leak) 가능성
옵저버를 구독에서 해지하지 않고 그대로 남겨둘 경우, 더 이상 필요 없는 객체가 계속 메모리를 차지하게 되어 메모리 누수가 발생할 수 있습니다. 이를 방지하려면, 구독 해지를 적절히 관리해야 합니다.
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를 구현해보겠습니다.
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() 메서드를 실행시킵니다.
아래 이미지는 위 내용을 나타내는 이미지입니다.
옵저버 패턴은 객체 간의 일대다 의존 관계를 정의하여, 한 객체의 상태가 변경될 때 그 객체에 의존하는 모든 객체에 자동으로 알림을 주는 디자인 패턴입니다.