← Posts

GoF · Behavioral · Memento

Memento

객체의 내부 상태를 캡슐화를 깨지 않고 스냅샷으로 저장해뒀다가 되돌리는 행위 패턴.

목차
  1. 되돌리려는데 캡슐화가 걸린다
  2. 메멘토 = 상태를 캡슐화한 채 스냅샷으로
  3. 세 역할: Originator, Memento, Caretaker
  4. 저장: 원본이 스냅샷을 만든다
  5. 복원: 스냅샷을 되돌린다
  6. 캡슐화를 지킨다
  7. 실무: undo 스택과 체크포인트
  8. 정리

되돌리려는데 캡슐화가 걸린다

텍스트 에디터에 Ctrl+Z를 달려고 한다. 되돌리려면 어느 시점의 상태를 어딘가에 저장해뒀다가, 나중에 그대로 되살려야 한다.

java
public class Editor {
    private String content;
    private int cursor;
    // 저장하려면 이 둘을 밖으로 다 꺼내야 하나?
}

가장 쉬운 방법은 contentcursorpublic으로 열거나 getter/setter를 다 뚫어, 바깥 히스토리 객체가 값을 읽어다 보관하고 나중에 도로 써넣는 것이다. 그런데 이러면 캡슐화가 깨진다. 에디터의 내부 필드가 무엇인지 바깥이 다 알게 되고, 필드가 하나 늘 때마다 저장·복원 코드를 밖에서 따라 고쳐야 한다. 되돌리기 하나 붙이려다 객체의 속을 통째로 열어젖히는 셈이다.

메멘토 = 상태를 캡슐화한 채 스냅샷으로

메멘토 패턴이 이걸 푼다.

객체의 내부 상태를 불투명한 스냅샷으로 뽑아 밖에 보관해뒀다가, 필요할 때 그 스냅샷으로 상태를 되돌린다.

핵심은 스냅샷의 속을 밖에서 못 본다는 것이다. 바깥은 스냅샷을 그저 상자로 받아 보관만 하고, 그 상자를 열어 내부 상태를 만들고 되돌리는 건 원래 객체만 한다. 그래서 저장·복원이 되면서도 캡슐화는 그대로 남는다.

세 역할: Originator, Memento, Caretaker

메멘토는 셋으로 나뉜다.

  • Originator(원본) - 상태를 가진 객체. 자기 상태로 메멘토를 만들고, 메멘토를 받아 자기 상태를 되돌린다. 여기선 Editor.
  • Memento(메멘토) - 상태의 스냅샷. 내부는 Originator만 들여다본다.
  • Caretaker(보관자) - 메멘토를 보관하되 속은 안 열어본다. 여기선 undo 스택을 쥔 History.
diagramdiagram

보관자는 메멘토를 상자째 쌓고 꺼낼 뿐, 상자 안을 모른다. 그래서 원본의 내부 구조가 바뀌어도 보관자는 그대로다.

저장: 원본이 스냅샷을 만든다

메멘토를 만드는 건 원본 자신이다. 자기 필드를 아는 건 자기뿐이니까.

java
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);
    }
}

메멘토 자체는 값만 품은 불변 객체다. 한번 만들어지면 바뀌지 않는다.

java
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는 못 연다. 이게 “불투명한 상자”를 만드는 장치다.

복원: 스냅샷을 되돌린다

되돌리는 것도 원본이 한다. 메멘토를 받아 자기 필드에 되써넣는다.

java
public void restore(EditorMemento m) {
    this.content = m.getContent();
    this.cursor = m.getCursor();
}

보관자는 이 흐름을 지휘만 한다. 스택에서 이전 스냅샷을 꺼내 원본에게 건넬 뿐, 그 값이 무엇인지는 모른다.

java
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());    // 직전 스냅샷으로 되돌린다
        }
    }
}

쓰는 쪽은 이렇게 된다.

java
Editor editor = new Editor();
History history = new History();

editor.type("안녕");
history.backup(editor);     // 여기까지를 기억
editor.type(" 세상");
history.undo(editor);       // "안녕"으로 되돌아온다

캡슐화를 지킨다

값어치는 캡슐화에 있다. 되돌리기를 붙이면서도 에디터의 내부 필드는 밖으로 새지 않았다. Historycontentcursor라는 이름조차 모른다.

그래서 원본의 내부가 바뀌어도 보관자는 안전하다. 에디터에 필드가 하나 늘면 고칠 곳은 saverestore, 즉 에디터와 메멘토뿐이다. 스택으로 상자를 쌓고 꺼내는 History는 손댈 게 없다. 상태를 밖에 저장하는 일과 그 상태의 속을 아는 일을 갈라놓은 것이 이 패턴이 사는 이유다.

실무: undo 스택과 체크포인트

실무에서 메멘토는 undo/redo 스택의 뼈대다. 편집기, 그래픽 툴, 폼 입력의 되돌리기가 전부 이 구조다. 상태를 스냅샷으로 쌓아두고 pop으로 되감는다.

게임의 세이브/체크포인트, 트랜잭션의 롤백을 위한 상태 스냅샷도 같은 발상이다. 다만 주의할 게 있다. 스냅샷이 커지거나 자주 찍히면 메모리를 많이 먹는다. 매 글자마다 전체 문서를 복사해 쌓으면 금세 불어난다. 그래서 실무에선 스냅샷 개수를 제한하거나, 전체 대신 바뀐 부분만 저장하는 식으로 절충한다.

정리

메멘토란내부 상태를 불투명한 스냅샷으로 저장해뒀다 되돌린다
문제되돌리려고 내부 필드를 밖으로 다 꺼내면 캡슐화가 깨진다
세 역할Originator(상태 소유)·Memento(스냅샷)·Caretaker(보관)
저장·복원스냅샷을 만들고 되돌리는 건 원본만, 보관자는 상자만 쥔다
효과undo가 되면서도 내부 구조는 밖으로 새지 않는다
주의스냅샷이 크거나 잦으면 메모리를 먹는다

내부 상태를 밖으로 열어젖히지 않고, 원본만 열 수 있는 스냅샷 상자로 저장해뒀다 되돌리는 것 - 그게 메멘토다.

관련 글