jblog
Memento 패턴
ArchitectureGuru 디자인 패턴 #17

Memento 패턴

객체의 상태를 캡슐화를 깨뜨리지 않고 저장하고 복원하는 Memento 패턴을 알아봅니다.

2026-01-2311 min readdesign-pattern, behavioral, architecture

#보스전 앞에서 세이브하는 이유

게임을 해본 사람이라면 누구나 이 루틴을 알고 있다.

  1. 보스방 앞에 도착한다
  2. 세이브한다 (필수)
  3. 보스에게 돌진한다
  4. 죽는다
  5. 로드한다
  6. 3번으로 돌아간다

게임 세이브

이 세이브/로드 시스템이 바로 오늘 다룰 Memento 패턴의 본질이다. 현재 상태를 어딘가에 저장해두고, 필요할 때 그 시점으로 되돌리는 것.

코드에서도 이런 상황은 흔하다. 텍스트 에디터의 Ctrl+Z, Git의 커밋, 브라우저의 뒤로가기 — 전부 "이전 상태로 되돌리기"다.

visible: false

Memento란?

객체의 내부 상태를 캡슐화를 위반하지 않고 캡처하여 저장하고, 나중에 해당 상태로 복원할 수 있게 하는 패턴

핵심 키워드는 캡슐화를 위반하지 않고다. 객체의 private 필드를 외부에 노출하지 않으면서도 상태를 저장할 수 있어야 한다.

세 가지 역할이 등장한다.

  1. Originator — 상태를 가진 원본 객체. 스냅샷을 생성하고 복원한다
  2. Memento — 상태 스냅샷. 불변 객체로, Originator만 내용을 읽을 수 있다
  3. Caretaker — Memento를 보관하는 관리자. 내용은 모르고 저장/전달만 한다

visible: false

코드로 보기

텍스트 에디터 with Undo

가장 직관적인 예시인 텍스트 에디터의 Undo 기능을 구현해보자.

// Memento — 에디터 상태 스냅샷
class EditorSnapshot {
  private readonly content: string;
  private readonly cursorPosition: number;
  private readonly selectionStart: number | null;
  private readonly selectionEnd: number | null;
  private readonly timestamp: Date;
 
  constructor(
    content: string,
    cursorPosition: number,
    selectionStart: number | null,
    selectionEnd: number | null,
  ) {
    this.content = content;
    this.cursorPosition = cursorPosition;
    this.selectionStart = selectionStart;
    this.selectionEnd = selectionEnd;
    this.timestamp = new Date();
  }
 
  // Originator만 접근하도록 의도된 메서드들
  getContent(): string {
    return this.content;
  }
 
  getCursorPosition(): number {
    return this.cursorPosition;
  }
 
  getSelectionStart(): number | null {
    return this.selectionStart;
  }
 
  getSelectionEnd(): number | null {
    return this.selectionEnd;
  }
 
  getTimestamp(): Date {
    return this.timestamp;
  }
 
  // Caretaker가 표시용으로만 사용
  getDescription(): string {
    const preview = this.content.slice(0, 30);
    return `[${this.timestamp.toLocaleTimeString()}] "${preview}${this.content.length > 30 ? "..." : ""}"`;
  }
}
// Originator — 텍스트 에디터
class TextEditor {
  private content: string = "";
  private cursorPosition: number = 0;
  private selectionStart: number | null = null;
  private selectionEnd: number | null = null;
 
  // 텍스트 입력
  type(text: string): void {
    const before = this.content.slice(0, this.cursorPosition);
    const after = this.content.slice(this.cursorPosition);
    this.content = before + text + after;
    this.cursorPosition += text.length;
    this.clearSelection();
  }
 
  // 텍스트 삭제
  deleteSelection(): void {
    if (this.selectionStart !== null && this.selectionEnd !== null) {
      const before = this.content.slice(0, this.selectionStart);
      const after = this.content.slice(this.selectionEnd);
      this.content = before + after;
      this.cursorPosition = this.selectionStart;
      this.clearSelection();
    }
  }
 
  // 커서 이동
  moveCursor(position: number): void {
    this.cursorPosition = Math.max(0, Math.min(position, this.content.length));
  }
 
  // 선택 영역 설정
  select(start: number, end: number): void {
    this.selectionStart = start;
    this.selectionEnd = end;
  }
 
  private clearSelection(): void {
    this.selectionStart = null;
    this.selectionEnd = null;
  }
 
  // ⭐ 스냅샷 생성 (save)
  save(): EditorSnapshot {
    return new EditorSnapshot(
      this.content,
      this.cursorPosition,
      this.selectionStart,
      this.selectionEnd,
    );
  }
 
  // ⭐ 스냅샷 복원 (restore)
  restore(snapshot: EditorSnapshot): void {
    this.content = snapshot.getContent();
    this.cursorPosition = snapshot.getCursorPosition();
    this.selectionStart = snapshot.getSelectionStart();
    this.selectionEnd = snapshot.getSelectionEnd();
  }
 
  getContent(): string {
    return this.content;
  }
}
// Caretaker — Undo/Redo 히스토리 관리자
class EditorHistory {
  private undoStack: EditorSnapshot[] = [];
  private redoStack: EditorSnapshot[] = [];
 
  // 현재 상태를 저장
  push(snapshot: EditorSnapshot): void {
    this.undoStack.push(snapshot);
    this.redoStack = []; // 새 액션이 발생하면 redo 스택 초기화
  }
 
  // Undo — 이전 스냅샷 반환
  undo(): EditorSnapshot | null {
    const snapshot = this.undoStack.pop();
    if (snapshot) {
      this.redoStack.push(snapshot);
    }
    return snapshot ?? null;
  }
 
  // Redo — 되돌린 스냅샷 다시 적용
  redo(): EditorSnapshot | null {
    const snapshot = this.redoStack.pop();
    if (snapshot) {
      this.undoStack.push(snapshot);
    }
    return snapshot ?? null;
  }
 
  // 히스토리 목록 표시
  showHistory(): void {
    console.log("📚 히스토리:");
    this.undoStack.forEach((s, i) => {
      console.log(`  ${i + 1}. ${s.getDescription()}`);
    });
  }
}

사용해보기

const editor = new TextEditor();
const history = new EditorHistory();
 
// 편집 시작
history.push(editor.save()); // 빈 상태 저장
editor.type("Hello, World!");
console.log(editor.getContent()); // "Hello, World!"
 
history.push(editor.save()); // "Hello, World!" 저장
editor.type(" Welcome to TypeScript.");
console.log(editor.getContent()); // "Hello, World! Welcome to TypeScript."
 
history.push(editor.save()); // 현재 상태 저장
editor.type(" Let's learn Memento Pattern!");
console.log(editor.getContent());
// "Hello, World! Welcome to TypeScript. Let's learn Memento Pattern!"
 
// ⭐ Ctrl+Z! Undo!
const snapshot = history.undo();
if (snapshot) {
  editor.restore(snapshot);
}
console.log(editor.getContent());
// "Hello, World! Welcome to TypeScript."
 
// 한 번 더 Undo
const snapshot2 = history.undo();
if (snapshot2) {
  editor.restore(snapshot2);
}
console.log(editor.getContent());
// "Hello, World!"
 
// Ctrl+Shift+Z! Redo!
const snapshot3 = history.redo();
if (snapshot3) {
  editor.restore(snapshot3);
}
console.log(editor.getContent());
// "Hello, World! Welcome to TypeScript."

Caretaker인 EditorHistoryEditorSnapshot의 내부 데이터에 대해 전혀 모른다. 그냥 스택에 넣고 빼기만 할 뿐이다. 상태의 저장과 복원은 오직 TextEditor(Originator)만 한다. 이것이 캡슐화를 지키는 핵심이다.

Undo 성공

visible: false

실전에서 만나는 Memento

사실 Memento 패턴은 우리가 매일 쓰는 곳에 숨어있다.

1. Ctrl+Z — 모든 에디터의 Undo

VS Code에서 코드를 쓰다가 Ctrl+Z를 누르면, 이전 스냅샷으로 복원된다. 위에서 구현한 것과 동일한 원리다.

2. Git 커밋

git commit -m "보스전 앞 세이브"
# ... 코드를 마구 수정 ...
git checkout .  # 로드!

Git의 각 커밋이 프로젝트 전체의 스냅샷(Memento)이고, Git 자체가 Caretaker다.

3. 브라우저 히스토리

// 브라우저의 history API도 Memento 패턴
history.pushState({ page: 1 }, "title", "/page1"); // 상태 저장
history.back(); // 이전 상태로 복원
 
window.addEventListener("popstate", (event) => {
  // event.state가 바로 Memento!
  console.log("복원된 상태:", event.state);
});

4. 직렬화를 활용한 간단한 구현

실무에서는 전체 객체를 deep clone하는 방식으로 간단하게 구현하기도 한다.

class SimpleMemento<T> {
  private readonly state: string;
  private readonly timestamp: Date;
 
  constructor(state: T) {
    this.state = JSON.stringify(state); // 직렬화로 깊은 복사
    this.timestamp = new Date();
  }
 
  getState(): T {
    return JSON.parse(this.state);
  }
 
  getTimestamp(): Date {
    return this.timestamp;
  }
}
 
// 사용
class FormState {
  private history: SimpleMemento<Record<string, any>>[] = [];
 
  save(formData: Record<string, any>): void {
    this.history.push(new SimpleMemento(formData));
  }
 
  undo(): Record<string, any> | null {
    this.history.pop(); // 현재 상태 버리기
    const prev = this.history[this.history.length - 1];
    return prev?.getState() ?? null;
  }
}

visible: false

Command 패턴과 뭐가 다른가?

이 질문은 정말 많이 나온다. 둘 다 "되돌리기"와 관련이 있기 때문이다.

구분CommandMemento
저장하는 것연산(Operation)상태(State)
Undo 방식역연산을 실행이전 스냅샷으로 교체
예시"x를 5만큼 이동" → Undo: "x를 -5만큼 이동""x의 위치는 (10, 20)" → 복원
메모리가벼움 (연산만 기록)무거울 수 있음 (전체 상태 기록)
복잡도역연산 구현이 어려울 수 있음구현은 단순하지만 메모리 사용량 주의

간단히 말해서,

  • Command는 "뭘 했는지"를 기록하고, 반대로 실행해서 되돌린다
  • Memento는 "어땠는지"를 기록하고, 그 상태로 통째로 교체한다

두 패턴을 조합해서 쓰기도 한다. Command로 연산을 기록하되, 역연산이 복잡한 경우에는 Memento로 상태 스냅샷을 함께 저장하는 식이다.

visible: false

언제 사용하면 좋을까?

  1. 객체의 이전 상태를 복원할 수 있어야 할 때

    • Undo/Redo, 트랜잭션 롤백, 체크포인트
  2. 객체의 캡슐화를 깨지 않고 상태를 저장하고 싶을 때

    • getter로 모든 필드를 노출하지 않고도 스냅샷 가능
  3. 상태의 스냅샷이 필요한데, 역연산을 구현하기 어려울 때

    • Command 패턴의 Undo가 너무 복잡할 때 대안으로

visible: false

장단점

장점단점
캡슐화를 위반하지 않고 상태 저장 가능상태가 클 경우 메모리를 많이 소모
Originator 코드를 단순하게 유지스냅샷 생성/복원 비용이 클 수 있음
스냅샷 히스토리 관리가 직관적Caretaker가 Memento 생명주기를 관리해야 함

특히 메모리 문제는 실무에서 중요하다. 스냅샷 개수에 제한을 두거나, 변경된 부분만 저장하는 증분 스냅샷(Incremental Snapshot) 방식을 고려해야 한다.

visible: false

정리하며

Memento는 객체의 상태를 스냅샷으로 저장하고 복원하는 패턴이다.

핵심을 한 줄로 요약하면,

"세이브 포인트를 만들어라. 그리고 언제든 돌아갈 수 있게 하라."

보스전 앞에서 세이브 안 하고 돌진하는 건 용감한 게 아니라 무모한 것이다. 코드도 마찬가지다. 되돌릴 수 없는 작업 앞에서는 항상 스냅샷을 남기자.

세이브 완료

visible: false

#Reference

Refactoring Guru - Memento

refactoring.guru

댓글

댓글을 불러오는 중...