GoF 디자인 패턴: 행위 패턴(3) - 책임 연쇄, 메멘토 | 타입스크립트 예시

정처기디자인 패턴GoF행위 패턴책임 연쇄 패턴메멘토 패턴객체지향TypeScript
읽는데 약 9분 정도 소요
처음 쓰여진 날: 2025-07-05
마지막으로 고쳐진 날: 2025-07-05
이 글을 보러온 횟수: 26

요약

GoF 디자인 패턴 중 행위 패턴의 세 번째 파트입니다. 요청을 객체 체인으로 전달하는 책임 연쇄(Chain of Responsibility) 패턴과, 객체의 상태를 저장하고 복원하여 캡슐화를 유지하는 메멘토(Memento) 패턴의 개념과 TypeScript 예시를 알아봅니다.

지난 행위 패턴 (2)편에서는 커맨드(Command) 패턴템플릿 메서드(Template Method) 패턴을 살펴보았습니다. 요청을 객체로 캡슐화하고, 알고리즘의 뼈대를 정의하는 방법을 배웠다면, 이번에는 요청을 여러 객체에게 처리할 기회를 주거나 객체의 상태를 안전하게 저장하는 패턴들을 만나보겠습니다.

이번 포스트에서는 다음과 같은 패턴들을 다룹니다.

  • ⛓️ 책임 연쇄 (Chain of Responsibility) 패턴: 요청을 보내는 객체와 처리하는 객체를 분리하고, 요청을 처리할 수 있는 객체들을 사슬처럼 엮습니다.
  • 💾 메멘토 (Memento) 패턴: 객체의 내부 상태를 외부에 노출하지 않으면서, 상태를 저장하고 복원할 수 있게 합니다.

이 패턴들이 어떻게 시스템의 결합도를 낮추고 유연성을 높이는지 확인해 보시죠!

⛓️ 5. 책임 연쇄 (Chain of Responsibility) 패턴

책임 연쇄 패턴은 요청을 처리할 수 있는 객체들을 체인(사슬)으로 연결하여, 요청이 들어오면 체인을 따라 순서대로 처리 기회를 넘기는 패턴입니다. 요청을 보내는 클라이언트는 체인의 시작점에만 요청을 전달할 뿐, 실제로 어떤 객체가 그 요청을 처리하는지는 알 필요가 없습니다.

이 패턴은 두 가지 대표적인 방식으로 활용될 수 있습니다.

  1. 독점적 처리: 체인을 따라가다 하나의 처리기가 요청을 처리하면 거기서 종료됩니다. (예: 승인/결재 시스템)
  2. 포괄적 처리: 하나의 요청을 여러 처리기가 모두 순차적으로 처리합니다. (예: 웹 프레임워크 미들웨어)

🏗️ 기본 구조

  • 📜 Handler: 모든 처리기(ConcreteHandler)가 구현해야 할 공통 인터페이스입니다. 다음 처리기를 연결하기 위한 setNext() 메서드와 요청을 처리하는 handle() 메서드를 정의합니다.
  • ConcreteHandler: Handler 인터페이스를 구현하는 구체적인 처리기입니다. handle() 메서드 내에서 자신의 역할을 수행하고, 다음 처리기에게 요청을 넘길지 결정합니다.
  • Client: 요청을 생성하고, 체인의 첫 번째 Handler에게 요청을 전달하는 역할을 합니다.

💻 예시 1: 고객센터 문의 시스템 (독점적 처리)

이 예시는 체인을 따라가다 하나의 처리기가 요청을 처리하면 거기서 종료되는 방식입니다.

typescript
// Handler: 모든 처리기의 공통 인터페이스
abstract class SupportHandler {
  protected nextHandler: SupportHandler | null = null;

  setNext(handler: SupportHandler): SupportHandler {
    this.nextHandler = handler;
    return handler;
  }

  handle(query: { type: string; message: string }): void {
    if (this.nextHandler) {
      this.nextHandler.handle(query);
    } else {
      console.log("아무도 처리할 수 없는 문의입니다.");
    }
  }
}

// ConcreteHandler: 기술 지원팀
class TechSupportHandler extends SupportHandler {
  handle(query: { type: string; message: string }): void {
    if (query.type === "tech") {
      console.log(`[기술 지원팀] "${query.message}" 문의를 처리합니다.`);
    } else {
      super.handle(query); // 처리 못하면 다음으로 넘김
    }
  }
}
// ... 다른 핸들러들 (Billing, General) ...

클라이언트 코드에서 처리기들을 체인으로 엮고 요청을 보내면, 요청은 자신을 처리할 수 있는 단 하나의 핸들러를 만날 때까지 체인을 따라 이동합니다.

💻 예시 2: 웹 서버 미들웨어 (포괄적 처리)

하나의 요청을 여러 처리기가 모두 순차적으로 처리하는 방식입니다. 각 처리기는 자신의 역할을 수행한 뒤, 요청을 종료시키지 않고 무조건 다음 처리기에게 전달하여 작업이 누적되도록 합니다.

typescript
// 처리될 요청 객체
interface HttpRequest {
  path: string;
  user?: { id: number; name: string }; // 인증 미들웨어가 채워줄 속성
}

// Handler 추상 클래스
abstract class Middleware {
  protected next: Middleware | null = null;

  setNext(middleware: Middleware): Middleware {
    this.next = middleware;
    return middleware;
  }

  // 이제 handle은 항상 다음을 호출하는 구조가 됨
  handle(request: HttpRequest): void {
    if (this.next) {
      this.next.handle(request);
    }
  }
}

// ConcreteHandler 1: 인증 미들웨어
class AuthMiddleware extends Middleware {
  handle(request: HttpRequest): void {
    console.log("1. [인증] 사용자 토큰 검사 완료. 요청에 사용자 정보 추가.");
    request.user = { id: 123, name: "김재현" };
    super.handle(request); // 다음으로 전달
  }
}

// ConcreteHandler 2: 로깅 미들웨어
class LoggingMiddleware extends Middleware {
  handle(request: HttpRequest): void {
    console.log(`2. [로깅] Path: ${request.path}, User: ${request.user?.name}`);
    super.handle(request); // 다음으로 전달
  }
}

// 최종 처리기
class AppController extends Middleware {
  handle(request: HttpRequest): void {
    console.log("3. [컨트롤러] 최종 비즈니스 로직 실행.");
    // 체인의 끝이므로 super.handle()을 호출하지 않음
  }
}

// 클라이언트 코드
const auth = new AuthMiddleware();
const logging = new LoggingMiddleware();
const controller = new AppController();

// 체인 연결: 인증 -> 로깅 -> 컨트롤러
auth.setNext(logging).setNext(controller);

const request: HttpRequest = { path: "/profile" };
console.log("--- 클라이언트 요청 발생 ---");
auth.handle(request);

이처럼 단일 요청이 체인을 따라가며 인증, 로깅 등 여러 작업을 순차적으로 적용받는 구조를 만들 수 있습니다.

책임 연쇄 패턴 중요 키워드 🔑

  • ⛓️ 요청자와 수신자를 분리하고, 수신자들을 사슬처럼 연결합니다.
  • 🤝 결합도를 낮춥니다: 요청자는 어떤 수신자가 요청을 처리하는지 알 필요가 없습니다.
  • ➡️ 하나의 요청을 여러 객체가 처리할 수 있습니다. (미들웨어 방식)
  • 💡 새로운 처리기를 추가하거나 순서를 변경하는 것이 유연합니다.

💾 6. 메멘토 (Memento) 패턴

메멘토 패턴은 객체의 내부 상태를 외부에 노출시키지 않으면서, 객체의 특정 시점 상태를 스냅샷처럼 저장해 두었다가 필요할 때 다시 복원할 수 있게 하는 패턴입니다. 이 패턴의 가장 중요한 목표는 캡슐화(Encapsulation)를 유지하는 것입니다.

'게임 저장(Save Game)' 🎮 기능을 생각하면 완벽합니다. 플레이어는 게임의 현재 상태(주인공의 위치, 레벨, 아이템 등)를 저장했다가, 나중에 그 시점부터 다시 시작하고 싶을 때 불러올 수 있습니다. 이때 게임의 복잡한 내부 데이터 구조가 외부에 직접 노출되지 않고, '세이브 파일'이라는 불투명한 객체(메멘토)를 통해 안전하게 상태가 저장되고 복원됩니다.

🏗️ 기본 구조

  • 🎬 Originator: 자신의 상태를 저장하고 복원해야 하는 원본 객체입니다. save() 메서드를 통해 현재 상태를 담은 Memento 객체를 생성하고, restore() 메서드를 통해 Memento 객체로부터 자신의 상태를 복원합니다.
  • 💾 Memento: Originator의 내부 상태를 저장하는 객체입니다. Originator만이 메멘토의 모든 데이터에 접근할 수 있어야 하며, 다른 객체들(특히 Caretaker)은 메멘토의 내부를 들여다볼 수 없습니다.
  • 📚 Caretaker: Memento 객체를 보관하고 관리하는 역할을 합니다. 하지만 메멘토의 내용을 검사하거나 수정하지는 않습니다. 그저 Originator로부터 메멘토를 받아 저장해 두었다가, 나중에 Originator에게 다시 돌려줄 뿐입니다. (예: 실행 취소 내역을 관리하는 History 객체)

예시: 간단한 텍스트 편집기 만들기

글을 작성하고, 특정 시점의 내용을 저장(스냅샷)했다가 되돌리는(undo) 간단한 텍스트 편집기를 만들어 보겠습니다.

먼저, 편집기의 상태를 저장할 Memento 클래스를 정의합니다.

typescript
// Memento: 편집기의 상태(내용)를 저장
class EditorMemento {
  // readonly를 사용하여 외부에서 수정 불가능하도록 함
  constructor(public readonly content: string) {}
}

다음으로, 상태를 저장하고 복원할 주체인 Originator, 즉 Editor 클래스를 만듭니다.

typescript
// Originator: 원본 객체
class Editor {
  private content: string = "";

  write(text: string): void {
    this.content += text;
  }

  getContent(): string {
    return this.content;
  }

  // 현재 상태를 메멘토에 저장
  save(): EditorMemento {
    console.log("상태 저장: ", this.content);
    return new EditorMemento(this.content);
  }

  // 메멘토로부터 상태를 복원
  restore(memento: EditorMemento): void {
    this.content = memento.content;
    console.log("상태 복원: ", this.content);
  }
}

마지막으로, 메멘토들을 관리할 CaretakerHistory 클래스를 만듭니다.

typescript
// Caretaker: 메멘토를 보관하지만 내용은 모름
class History {
  private mementos: EditorMemento[] = [];
  private editor: Editor;

  constructor(editor: Editor) {
    this.editor = editor;
  }

  push(): void {
    this.mementos.push(this.editor.save());
  }

  undo(): void {
    // 마지막 상태(현재 상태)를 버리기 위해 pop
    this.mementos.pop();

    // 그 이전 상태를 가져와 복원
    const lastMemento = this.mementos[this.mementos.length - 1];
    if (lastMemento) {
      this.editor.restore(lastMemento);
    } else {
      // 스택에 아무것도 없으면 초기 상태로 복원
      this.editor.restore(new EditorMemento(""));
    }
  }
}

// 클라이언트 코드
const editor = new Editor();
const history = new History(editor);

// 1. 첫 번째 문장 작성 및 저장
editor.write("안녕하세요. ");
history.push(); // "안녕하세요. " 상태 저장

// 2. 두 번째 문장 작성 및 저장
editor.write("메멘토 패턴입니다.");
history.push(); // "안녕하세요. 메멘토 패턴입니다." 상태 저장

// 3. 현재 내용 확인
console.log("현재 내용: ", editor.getContent());
// 출력: 현재 내용:  안녕하세요. 메멘토 패턴입니다.

// 4. 실행 취소 (Undo)
history.undo();
console.log("실행 취소 후 내용: ", editor.getContent());
// 출력:
// 상태 복원:  안녕하세요.
// 실행 취소 후 내용:  안녕하세요.

// 5. 한 번 더 실행 취소 (Undo)
history.undo();
console.log("두 번째 실행 취소 후 내용: ", editor.getContent());
// 출력:
// 상태 복원:
// 두 번째 실행 취소 후 내용:

History 객체는 EditorMemento 안에 content가 있는지조차 모릅니다. 그저 메멘토 객체를 스택에 넣고 빼는 역할만 충실히 수행합니다. 이 덕분에 Editor의 내부 상태(content)는 외부로부터 안전하게 보호되면서(캡슐화), 실행 취소와 같은 강력한 상태 관리 기능을 구현할 수 있습니다.

메멘토 패턴 중요 키워드 🔑

  • 💾 캡슐화를 위반하지 않고 객체의 내부 상태를 외부에 저장합니다.
  • 🎬 스냅샷과 복원: 특정 시점의 상태를 저장하고, 나중에 그 상태로 되돌릴 수 있습니다.
  • ↩️ 실행 취소(Undo/Redo), 트랜잭션, 상태 저장/불러오기 기능에 매우 유용합니다.
  • 역할 분리: 상태를 만드는 Originator, 상태를 저장하는 Memento, 상태를 관리하는 Caretaker로 역할이 명확히 나뉩니다.