포스트

Memento Pattern

객체의 내부 상태를 캡슐화를 깨지 않고 스냅샷으로 저장했다가 나중에 되돌리는 패턴. 요청을 캡슐화하는 Command와 조합해 undo를 구현하는 경계까지 비교한다.

Memento Pattern

난이도 중급 · 선행 Command

한 줄 요약

객체의 내부 상태를 스냅샷으로 통째로 저장해 두었다가, 필요할 때 그 시점으로 되돌린다. 핵심은 상태를 꺼내 저장하면서도 캡슐화를 깨지 않는 것 — 바깥은 스냅샷의 내용을 들여다볼 수 없다.

어떤 문제를 푸는가

텍스트 에디터에 undo를 붙인다고 하자. 가장 쉬운 발상은 상태를 바깥에서 직접 들고 저장하는 것이다.

1
2
3
4
5
6
7
8
9
10
11
class Editor {
public:
    std::string content;   // undo 하려고 public으로 열어버림
    int cursor;
};

// 어딘가의 히스토리 관리 코드
std::vector<std::string> history;
history.push_back(editor.content);   // 내부를 직접 들여다본다
// ... 편집 ...
editor.content = history.back();      // 직접 되돌린다

이 코드의 통증:

  • undo를 위해 content, cursor를 전부 public으로 열어야 한다. 캡슐화가 무너진다.
  • 저장해야 할 상태가 늘면(선택 영역, 스크롤 위치…) 히스토리 코드가 에디터 내부를 더 깊이 알게 된다.
  • 에디터 내부 표현이 바뀌면 히스토리 코드가 같이 깨진다.

패턴 적용 후

상태를 담는 Memento 객체를 두고, 에디터(Originator)만 그 안을 채우고 읽을 수 있게 한다. 히스토리(Caretaker)는 메멘토를 받아 쌓아둘 뿐 내용은 모른다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Memento — 상태를 담지만 바깥엔 불투명하다
class EditorMemento {
    friend class Editor;          // Editor만 내부에 접근
    std::string content;
    int cursor;
    EditorMemento(std::string c, int cur) : content(std::move(c)), cursor(cur) {}
};

// Originator — 상태의 주인
class Editor {
    std::string content;
    int cursor = 0;
public:
    void type(const std::string& text) { content += text; cursor = content.size(); }

    EditorMemento save() const { return {content, cursor}; }      // 스냅샷 생성
    void restore(const EditorMemento& m) { content = m.content; cursor = m.cursor; }

    const std::string& text() const { return content; }
};

// Caretaker — 메멘토를 보관만 한다. 내용은 모른다.
class History {
    std::vector<EditorMemento> snapshots;
public:
    void push(EditorMemento m) { snapshots.push_back(std::move(m)); }
    EditorMemento pop() { auto m = snapshots.back(); snapshots.pop_back(); return m; }
    bool empty() const { return snapshots.empty(); }
};

int main() {
    Editor editor;
    History history;

    editor.type("Hello");
    history.push(editor.save());   // 스냅샷 저장

    editor.type(", World");
    std::cout << editor.text() << "\n";   // Hello, World

    editor.restore(history.pop());        // undo
    std::cout << editor.text() << "\n";   // Hello
}

달라진 점:

  • content, cursor는 다시 private. 캡슐화가 살아 있다.
  • History는 메멘토를 쌓고 꺼낼 뿐, 그 안에 뭐가 있는지 모른다. 에디터 내부 표현이 바뀌어도 History는 그대로다.

구조

  • Originator: 상태의 주인. 메멘토를 만들고(save) 그것으로 복원한다(restore) — Editor
  • Memento: 상태 스냅샷. Originator에게만 속을 보인다 — EditorMemento
  • Caretaker: 메멘토를 보관·관리하지만 내용은 모른다 — History
1
2
3
Originator ──save()──▶ Memento ──저장──▶ Caretaker
Originator ◀─restore()─ Memento ◀─꺼냄── Caretaker
   (속을 아는 건 Originator뿐, Caretaker는 불투명한 상자로 다룬다)

실전 사례

  • 에디터의 Ctrl+Z: 위 예제 그대로. 편집 직전 스냅샷을 쌓아 되돌린다.
  • 게임 세이브/체크포인트: 특정 시점의 전체 상태를 저장했다가 로드.
  • DB 트랜잭션의 세이브포인트/롤백: 스냅샷을 떠두고 실패 시 되돌리는 발상이 같다.

Command Pattern과의 차이 — 요청과 상태

둘은 경쟁하는 undo 알고리즘이 아니라 서로 다른 것을 캡슐화하는 패턴입니다. Command는 요청을 객체로 만들며 undo는 선택 기능이고, Memento는 이전 상태를 불투명한 snapshot으로 보관합니다.

 CommandMemento
캡슐화하는 것수행할 요청과 receiver·parameter특정 시점의 내부 상태
undo 방식역연산, 이전 값, Memento 등으로 선택 구현저장한 상태를 Originator가 복원
메모리보통 가볍다상태가 크면 무겁다
잘 맞는 때역연산이 명확할 때 (글자 삽입 ↔ 삭제)상태가 복잡해 역연산을 정의하기 어려울 때

둘은 자주 함께 쓰인다 — Command가 실행 전에 Memento로 스냅샷을 떠두고, undo 때 그 스냅샷으로 되돌리는 식. Command 패턴 글의 취소 메커니즘과 이어서 보면 좋다.

안티패턴 / 주의

  • 상태가 크면 스냅샷 비용이 폭발한다. 매 키 입력마다 문서 전체를 복사하면 메모리가 감당이 안 된다. 변경분(diff)만 저장하거나 일정 간격으로만 스냅샷을 뜨는 식으로 절충한다.
  • 메멘토 안의 포인터·참조를 얕게 복사하면 “복원했는데 같이 바뀌는” Prototype과 같은 함정에 빠진다. 스냅샷은 그 시점의 독립된 복사여야 한다.
  • 메멘토 내부를 Caretaker가 들여다보게 만들면 패턴의 핵심(캡슐화)이 무너진다. Caretaker에게는 불투명한 상자로 유지하라.

스스로 점검

1. undo를 위해 상태를 그냥 public으로 여는 것과 Memento의 차이는?

Memento는 상태를 외부에 저장하면서도 캡슐화를 지킨다. 스냅샷(메멘토)의 속은 주인(Originator)만 보고, 보관자(Caretaker)는 불투명한 상자로 다룬다. public으로 열면 내부 표현이 새어나가 결합이 생기고 표현 변경이 어려워진다.

2. Command의 undo와 Memento의 undo는 어떻게 다른가?

Command의 본질은 요청 캡슐화이고 undo는 선택 기능입니다. undo를 역연산이나 이전 값 저장으로 구현할 수도 있고, 실행 전에 Memento를 받아 두었다 복원할 수도 있습니다. Memento의 본질은 캡슐화를 깨지 않는 상태 snapshot입니다.

3. 매 키 입력마다 문서 전체를 스냅샷으로 저장하면 무슨 문제가 생기나?

상태가 클수록 메모리가 급격히 늘어난다. 변경분만 저장하거나 일정 간격으로만 스냅샷을 뜨는 식으로 비용을 조절해야 한다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.