cv.cm

코딩 · 디자인 패턴

메멘토 패턴 예제로 쉽게 이해하기

7분 분량

핵심 답변

메멘토(Memento)는 객체의 내부 상태를 메멘토라는 스냅숏 객체에 담아 두었다가 나중에 그 상태로 복원할 수 있게 하는 행동 패턴입니다. 상태를 가진 객체(오리지네이터)가 메멘토를 만들고 복원하며, 다른 객체(케어테이커)는 메멘토를 보관만 할 뿐 내부를 들여다보지 않습니다. 캡슐화를 깨지 않고 실행 취소, 체크포인트, 게임 저장을 구현하는 표준적인 방법입니다.

의도와 해결하는 문제

의도: 캡슐화를 위반하지 않고 객체의 내부 상태를 포착해 외부에 저장해 두어, 나중에 객체를 그 상태로 복원할 수 있게 합니다. 메멘토는 GoF(Gang of Four) 책에 실린 행동 패턴 중 하나입니다.

도형이 위치, 크기, 색상과 렌더링에 쓰이는 비공개 파생 값 목록을 가진 그림 그리기 앱을 상상해 봅시다. 실행 취소 버튼을 만들고 싶습니다. 실행 취소 관리자가 도형의 필드를 직접 복사할 수도 있지만, 그러면 모든 비공개 필드를 알아야 하고 도형 클래스가 바뀔 때마다 관리자가 깨집니다. 실행 취소만을 위해 모든 필드를 공개하면 아무 코드나 도형을 잘못된 상태로 만들 수 있습니다.

메멘토는 도형 스스로 스냅숏을 만들게 해서 이 문제를 해결합니다. 스냅숏 안에 무엇이 있고 어떻게 복원하는지는 도형만 압니다. 실행 취소 관리자는 스냅숏을 불투명한 객체로 취급해 목록에 저장해 두었다가, 사용자가 실행 취소를 누르면 하나를 돌려줍니다.

구조와 참여자

역할책임
오리지네이터(Originator)상태가 저장되는 객체로, 현재 상태의 메멘토를 만들고 메멘토로 스스로를 복원합니다
메멘토(Memento)오리지네이터 상태의 불변 스냅숏으로, 다른 객체에는 쓸모 있는 것을 노출하지 않습니다
케어테이커(Caretaker)언제 저장하고 언제 복원할지 결정하며 메멘토를 (예를 들어 스택에) 보관하지만, 내용을 읽거나 바꾸지 않습니다

핵심 아이디어는 두 개의 인터페이스입니다. 저장된 상태를 읽을 수 있는 오리지네이터용 넓은 인터페이스와, 메멘토를 들고만 있을 수 있는 나머지 모두를 위한 좁은 인터페이스입니다.

TypeScript 구현

메멘토 클래스는 JavaScript 비공개 필드(#state)를 사용하므로 케어테이커는 메멘토 객체에서 저장된 데이터를 읽을 수 없습니다. 읽기는 편집기만 호출하는 정적 메서드 EditorMemento.read를 통해 이루어집니다. 실제 프로젝트에서는 메멘토 클래스를 편집기와 같은 모듈에 두고 내보내지 마세요. 그러면 다른 모듈은 메멘토를 들고 있을 수는 있어도 열어 볼 수는 없습니다.

type EditorState = { text: string; cursor: number };

class EditorMemento {
  readonly #state: EditorState;
  constructor(state: EditorState) {
    this.#state = { ...state }; // copy, so later edits do not change the snapshot
  }
  static read(m: EditorMemento): EditorState {
    return { ...m.#state };
  }
}

class Editor {
  private state: EditorState = { text: "", cursor: 0 };
  type(words: string) {
    const text = this.state.text + words;
    this.state = { text, cursor: text.length };
  }
  get text() { return this.state.text; }
  save(): EditorMemento { return new EditorMemento(this.state); }
  restore(m: EditorMemento) { this.state = EditorMemento.read(m); }
}

class History {
  private stack: EditorMemento[] = [];
  constructor(private editor: Editor) {}
  backup() { this.stack.push(this.editor.save()); }
  undo() { const m = this.stack.pop(); if (m) this.editor.restore(m); }
}

const editor = new Editor();
const history = new History(editor);
history.backup(); editor.type("Hello");
history.backup(); editor.type(" world");
console.log(editor.text); // Hello world
history.undo();
console.log(editor.text); // Hello
history.undo();
console.log(JSON.stringify(editor.text)); // ""

History가 케어테이커입니다. 변경 전에 backup()을, 되돌릴 때 undo()를 호출하지만 text나 cursor는 전혀 보지 않습니다. 나중에 EditorState에 선택 범위를 추가해도 History는 바뀌지 않습니다.

실행 취소를 위한 메멘토와 커맨드 비교

실행 취소를 구현하는 흔한 방법은 두 가지이며, 둘을 함께 쓰는 경우도 많습니다.

접근 방식저장하는 것적합한 경우비용
메멘토변경 전 전체 상태의 스냅숏변경을 되돌리기 어렵거나 상태가 작을 때메모리가 상태 크기 곱하기 단계 수만큼 늘어납니다
커맨드연산과 이를 되돌리는 데 충분한 데이터각 연산에 간단한 역연산이 있을 때모든 커맨드에 올바른 실행 취소 로직이 필요합니다

실용적인 절충안은 대부분의 단계에서는 커맨드를 저장하고 N단계마다 전체 메멘토를 저장하거나, 커맨드가 바꾸는 상태의 일부만 저장하는 것입니다.

언제 쓰고 언제 쓰지 말아야 할까

  • 실행 취소, 다시 실행, 체크포인트, 게임 저장, 그리고 편집 내용을 되돌려야 하는 폼의 ‘취소’ 버튼에 사용합니다.
  • 메모리 안의 트랜잭션에 사용합니다. 위험한 연산 전에 스냅숏을 찍고 실패하면 복원합니다.
  • 케어테이커가 오리지네이터의 내부 필드에 의존하면 안 될 때 사용합니다.
  • 상태가 매우 크고 자주 바뀐다면, 기록 길이를 제한하거나 차이만 저장하지 않는 한 사용하지 않습니다.
  • 상태가 이미 단순한 불변 값이라면 격식을 모두 갖출 필요가 없습니다. 불변 데이터에서는 이전 값이 곧 스냅숏이므로 이전 값의 목록을 보관하는 것으로 충분합니다.

실제 사용 사례

  • 브라우저 History API: history.pushState(state, "", url)는 각 기록 항목에 상태 객체를 저장하고, 사용자가 이동하면 popstate 이벤트가 그것을 돌려줍니다.
  • Redux와 비슷한 스토어는 불변 상태 스냅숏을 유지하며, 도구가 이를 타임 트래블 디버깅에 사용합니다.
  • SQL 데이터베이스는 트랜잭션 안에서 SAVEPOINT를 지원하므로 이름 붙은 지점까지 작업을 롤백할 수 있습니다.
  • 텍스트 편집기, 그래픽 프로그램, 게임은 실행 취소, 자동 저장, 저장 슬롯을 위해 스냅숏을 저장합니다.

관련 패턴

패턴메멘토 패턴과의 관계
커맨드커맨드는 연산을 반대로 수행해 취소할 수 있고, 실행 전에 찍은 메멘토를 저장해 두었다가 undo에서 복원할 수도 있습니다.
프로토타입프로토타입은 객체 전체를 복제해 새 객체를 만들고, 메멘토는 같은 객체를 나중에 복원하기 위해 상태를 저장합니다.
이터레이터이터레이터는 현재 위치를 메멘토로 노출해 순회를 나중에 재개할 수 있습니다.
상태상태 패턴은 상태가 바뀔 때 객체의 동작을 바꾸고, 메멘토는 데이터를 기록하고 복원할 뿐입니다.

자주 묻는 질문

오리지네이터와 케어테이커의 차이는 무엇인가요?

오리지네이터는 상태를 소유하며 메멘토를 만들고 그로부터 복원할 수 있는 유일한 객체입니다. 케어테이커는 언제 저장하고 복원할지 결정하고 메멘토를 보관하지만, 메멘토를 불투명하게 취급합니다.

메멘토 패턴은 캡슐화를 깨나요?

아니요. 오히려 캡슐화를 지키는 것이 목적입니다. 오리지네이터의 내부 필드는 비공개로 남고, 메멘토가 이를 케어테이커로부터 숨기므로 저장된 상태는 오리지네이터만 읽을 수 있습니다.

메멘토 패턴에서 메모리 사용량은 어떻게 제한하나요?

스냅숏 개수에 상한을 두고 가장 오래된 것을 버리거나, 상태 중 바뀐 부분만 저장하거나, 작은 변경을 기록하는 커맨드와 메멘토를 함께 쓰세요.

JSON 직렬화도 메멘토인가요?

상태를 JSON으로 직렬화하는 것은 메멘토의 저장 형식이 될 수 있으며 디스크에 저장할 때 유용합니다. 패턴 자체는 형식이 아니라 누가 스냅숏을 만들고, 보관하고, 복원하는지에 관한 것입니다.

코딩 튜토리얼 전체