캡슐화를 깨지 않고 객체의 내부 상태를 저장했다가 복원하는 패턴.
세 역할
- Originator — 상태를 가진 객체. 메멘토를 만들고 메멘토로 복원한다
- Memento — 상태 스냅샷. 내부를 Originator 만 볼 수 있다
- Caretaker — 메멘토를 보관한다. 내용은 들여다보지 않는다
class Editor { // Originator
private String text;
Snapshot save() { return new Snapshot(text); }
void restore(Snapshot s) { this.text = s.text; }
class Snapshot { // Memento (내부 클래스)
private final String text; // 밖에서 못 본다
private Snapshot(String t) { this.text = t; }
}
}
Deque<Editor.Snapshot> history = new ArrayDeque<>(); // Caretaker
history.push(editor.save());
editor.restore(history.pop()); // 실행 취소
핵심은 '캡슐화를 깨지 않는다'
단순히 getter 로 상태를 다 꺼내 저장하면
- 내부 구조가 밖으로 노출된다
- 필드가 바뀌면 저장하는 쪽도 전부 바뀐다
Memento 는 상태를 '불투명한 객체' 로 넘긴다
- Caretaker 는 보관만 하고 내용은 모른다
대가 — 메모리
스냅샷마다 전체 상태를 복사하면 메모리가 빠르게 는다
완화
- 변경분만 저장 (delta·명령 기록)
- 히스토리 개수 상한
- 불변 객체면 구조 공유가 가능해 비용이 낮다
Command와 함께 쓴다
Command 로 '무엇을 했는지' 를 기록하고 Memento 로 '되돌릴 상태' 를 보관하면 완전한 undo/redo 가 된다
또는 Command 에 undo() 를 두어 역연산으로 되돌리는 방식도 있다 (역연산이 가능하면 메모리가 훨씬 적게 든다)
면접 함정
- ❌ "getter로 저장하면 같다" → 캡슐화가 깨진다는 점이 이 패턴의 존재 이유다.
- ❌ 직렬화와 동일시 → 직렬화는 저장 형식, Memento는 캡슐화 유지 설계다.
되돌리기의 두 전략
-
① 상태 저장 (Memento)
- 복원이 단순하다 / 메모리를 많이 쓴다
-
② 역연산 (Command.undo)
- 메모리가 적게 든다 / 모든 연산에 역연산이 있어야 한다
- 되돌릴 수 없는 것도 있다 (삭제한 파일 내용 · 외부 API 호출)
-
실무에서는 섞는다 — 되돌릴 수 있으면 역연산, 아니면 스냅샷
스냅샷 크기 줄이기
- 전체가 아니라 변경분만 (delta)
- 일정 간격 전체 스냅샷 + 사이는 delta (체크포인트)
- 불변 자료구조면 구조 공유로 비용이 크게 준다
- (바뀐 노드만 새로 만들고 나머지는 참조 공유)
직렬화와 혼동하지 않는다
-
직렬화 — 객체를 바이트로 바꾸는 '형식' 의 문제 (저장·전송)
-
Memento — 캡슐화를 유지하며 상태를 넘기는 '설계' 의 문제
-
Memento 를 직렬화로 구현할 수는 있으나, 직렬화한다고 Memento 인 것은 아니다