목차
되돌리려는데 캡슐화가 걸린다
텍스트 에디터에 Ctrl+Z를 달려고 한다. 되돌리려면 어느 시점의 상태를 어딘가에 저장해뒀다가, 나중에 그대로 되살려야 한다.
public class Editor {
private String content;
private int cursor;
// 저장하려면 이 둘을 밖으로 다 꺼내야 하나?
}가장 쉬운 방법은 content와 cursor를 public으로 열거나 getter/setter를 다 뚫어, 바깥 히스토리 객체가 값을 읽어다 보관하고 나중에 도로 써넣는 것이다. 그런데 이러면 캡슐화가 깨진다. 에디터의 내부 필드가 무엇인지 바깥이 다 알게 되고, 필드가 하나 늘 때마다 저장·복원 코드를 밖에서 따라 고쳐야 한다. 되돌리기 하나 붙이려다 객체의 속을 통째로 열어젖히는 셈이다.
메멘토 = 상태를 캡슐화한 채 스냅샷으로
메멘토 패턴이 이걸 푼다.
객체의 내부 상태를 불투명한 스냅샷으로 뽑아 밖에 보관해뒀다가, 필요할 때 그 스냅샷으로 상태를 되돌린다.
핵심은 스냅샷의 속을 밖에서 못 본다는 것이다. 바깥은 스냅샷을 그저 상자로 받아 보관만 하고, 그 상자를 열어 내부 상태를 만들고 되돌리는 건 원래 객체만 한다. 그래서 저장·복원이 되면서도 캡슐화는 그대로 남는다.
세 역할: Originator, Memento, Caretaker
메멘토는 셋으로 나뉜다.
- Originator(원본) - 상태를 가진 객체. 자기 상태로 메멘토를 만들고, 메멘토를 받아 자기 상태를 되돌린다. 여기선
Editor. - Memento(메멘토) - 상태의 스냅샷. 내부는 Originator만 들여다본다.
- Caretaker(보관자) - 메멘토를 보관하되 속은 안 열어본다. 여기선 undo 스택을 쥔
History.
보관자는 메멘토를 상자째 쌓고 꺼낼 뿐, 상자 안을 모른다. 그래서 원본의 내부 구조가 바뀌어도 보관자는 그대로다.
저장: 원본이 스냅샷을 만든다
메멘토를 만드는 건 원본 자신이다. 자기 필드를 아는 건 자기뿐이니까.
public class Editor {
private String content = "";
private int cursor = 0;
public void type(String text) {
content += text;
cursor = content.length();
}
public EditorMemento save() { // 지금 상태를 스냅샷으로
return new EditorMemento(content, cursor);
}
}메멘토 자체는 값만 품은 불변 객체다. 한번 만들어지면 바뀌지 않는다.
public class EditorMemento {
private final String content;
private final int cursor;
EditorMemento(String content, int cursor) { // 패키지 범위
this.content = content;
this.cursor = cursor;
}
String getContent() { return content; } // 원본만 부른다
int getCursor() { return cursor; }
}getter를 public이 아니라 패키지 범위로 둔 게 핵심이다. 같은 패키지의 Editor는 열어보지만, 바깥의 History는 못 연다. 이게 “불투명한 상자”를 만드는 장치다.
복원: 스냅샷을 되돌린다
되돌리는 것도 원본이 한다. 메멘토를 받아 자기 필드에 되써넣는다.
public void restore(EditorMemento m) {
this.content = m.getContent();
this.cursor = m.getCursor();
}보관자는 이 흐름을 지휘만 한다. 스택에서 이전 스냅샷을 꺼내 원본에게 건넬 뿐, 그 값이 무엇인지는 모른다.
public class History {
private final Deque<EditorMemento> stack = new ArrayDeque<>();
public void backup(Editor editor) {
stack.push(editor.save()); // 스냅샷을 쌓는다
}
public void undo(Editor editor) {
if (!stack.isEmpty()) {
editor.restore(stack.pop()); // 직전 스냅샷으로 되돌린다
}
}
}쓰는 쪽은 이렇게 된다.
Editor editor = new Editor();
History history = new History();
editor.type("안녕");
history.backup(editor); // 여기까지를 기억
editor.type(" 세상");
history.undo(editor); // "안녕"으로 되돌아온다캡슐화를 지킨다
값어치는 캡슐화에 있다. 되돌리기를 붙이면서도 에디터의 내부 필드는 밖으로 새지 않았다. History는 content나 cursor라는 이름조차 모른다.
그래서 원본의 내부가 바뀌어도 보관자는 안전하다. 에디터에 필드가 하나 늘면 고칠 곳은 save와 restore, 즉 에디터와 메멘토뿐이다. 스택으로 상자를 쌓고 꺼내는 History는 손댈 게 없다. 상태를 밖에 저장하는 일과 그 상태의 속을 아는 일을 갈라놓은 것이 이 패턴이 사는 이유다.
실무: undo 스택과 체크포인트
실무에서 메멘토는 undo/redo 스택의 뼈대다. 편집기, 그래픽 툴, 폼 입력의 되돌리기가 전부 이 구조다. 상태를 스냅샷으로 쌓아두고 pop으로 되감는다.
게임의 세이브/체크포인트, 트랜잭션의 롤백을 위한 상태 스냅샷도 같은 발상이다. 다만 주의할 게 있다. 스냅샷이 커지거나 자주 찍히면 메모리를 많이 먹는다. 매 글자마다 전체 문서를 복사해 쌓으면 금세 불어난다. 그래서 실무에선 스냅샷 개수를 제한하거나, 전체 대신 바뀐 부분만 저장하는 식으로 절충한다.
정리
| 메멘토란 | 내부 상태를 불투명한 스냅샷으로 저장해뒀다 되돌린다 |
| 문제 | 되돌리려고 내부 필드를 밖으로 다 꺼내면 캡슐화가 깨진다 |
| 세 역할 | Originator(상태 소유)·Memento(스냅샷)·Caretaker(보관) |
| 저장·복원 | 스냅샷을 만들고 되돌리는 건 원본만, 보관자는 상자만 쥔다 |
| 효과 | undo가 되면서도 내부 구조는 밖으로 새지 않는다 |
| 주의 | 스냅샷이 크거나 잦으면 메모리를 먹는다 |
내부 상태를 밖으로 열어젖히지 않고, 원본만 열 수 있는 스냅샷 상자로 저장해뒀다 되돌리는 것 - 그게 메멘토다.