포스트

Memento Pattern

객체의 내부 상태를 캡슐화를 깨지 않고 스냅샷으로 저장했다가 나중에 되돌리는 패턴. undo/redo의 본체. Command의 역연산 방식과 비교한다.

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를 푸는 두 갈래

undo를 구현하는 방법은 크게 둘인데, 접근이 정반대다.

 Command (역연산)Memento (스냅샷)
되돌리는 법한 동작의 반대 동작을 실행 (undo())이전 상태로 통째로 복원
저장하는 것수행한 명령(과 역연산에 필요한 정보)시점별 상태 전체
메모리보통 가볍다상태가 크면 무겁다
잘 맞는 때역연산이 명확할 때 (글자 삽입 ↔ 삭제)상태가 복잡해 역연산을 정의하기 어려울 때

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

안티패턴 / 주의

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

스스로 점검

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

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

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

Command는 한 동작의 역연산을 실행해 되돌리고(삽입↔삭제), Memento는 이전 상태로 통째로 복원한다. 역연산이 명확하면 Command가 가볍고, 상태가 복잡해 역연산 정의가 어려우면 Memento가 낫다. 둘을 조합하기도 한다.

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

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

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