트렌드브리프
메멘토 패턴, 캡슐화를 지키며 상태 되돌리기

메멘토 패턴, 캡슐화를 지키며 상태 되돌리기

· 게시 · 약 5분 분량

메멘토 패턴은 객체의 이전 상태를 캡슐화를 깨지 않고 저장·복원하는 행동 디자인 패턴이다. 오리지네이터·메멘토·케어테이커 세 역할과 자바 최소 예제로 실행 취소(Undo) 구현 원리를 정리한다.

목차
  1. 메멘토 패턴은 무엇을 해결하나
  2. 오리지네이터·메멘토·케어테이커, 세 역할
  3. 코드로 보는 최소 구현
  4. 언제 메멘토 패턴이 맞나

메멘토 패턴은 객체의 이전 상태를 저장해 뒀다가 나중에 그대로 되돌리는 행동(behavioral) 디자인 패턴이다. 핵심은 그 상태를 바깥으로 드러내지 않고 되돌린다는 점이다. 텍스트 편집기의 실행 취소(Ctrl+Z)나 게임의 세이브·로드가 대표적인 쓰임이다. 디자인 패턴 해설로 널리 쓰이는 리팩토링 구루(refactoring.guru)는 이 패턴을 "구현 세부를 드러내지 않고 객체의 이전 상태를 저장·복원하게 해 주는 패턴"으로 정의한다. 이 글은 패턴의 세 역할과 자바 최소 예제, 그리고 언제 쓰고 언제 다른 방법을 고를지에 집중한다. 언어별 직렬화 방식이나 프레임워크에 내장된 Undo API는 다루지 않는다.

메멘토 패턴은 무엇을 해결하나

상태를 되돌리되 캡슐화를 깨지 않게 해 준다. 이것이 이 패턴의 존재 이유다. 실행 취소를 순진하게 구현하려면 되돌릴 객체의 내부 필드를 바깥에서 전부 복사해 둬야 한다. 그러려면 private으로 감춰 둔 값을 외부에 열어 줘야 하고, 그 순간 캡슐화는 무너진다. 필드가 늘거나 내부 자료구조가 바뀌면 밖에서 복사하던 코드도 같이 깨진다.

메멘토 패턴은 스냅샷 만드는 일을 객체 자신에게 맡긴다. 자기 상태를 가장 잘 아는 건 그 객체 자신이니, 스냅샷도 자신이 만들고 복원도 자신이 한다. 바깥 코드는 그 스냅샷을 불투명한 상자처럼 받아 보관만 하다가, 되돌릴 때 다시 건넨다. 내부를 들여다보지 않으니 캡슐화가 유지된다.

오리지네이터·메멘토·케어테이커, 세 역할

메멘토 패턴은 GoF(Gang of Four)의 『디자인 패턴』 책에 실린 23가지 패턴 중 하나로, 등장인물이 셋으로 정해져 있다.

역할을 이렇게 나누는 이유는 하나다. 상태를 아는 쪽(오리지네이터)과 이력을 관리하는 쪽(케어테이커)을 분리하면, 편집기 클래스는 텍스트 처리에만 집중하고 실행 취소 이력 관리는 다른 클래스가 맡는다.

객체가 상태를 봉인된 잠금 캡슐로 떠서 선반에 쌓고, 복원 화살표로 하나를 꺼내 객체를 되돌리는 선화 일러스트
상태를 봉인된 캡슐로 떠서 보관하고 되돌린다 — 속을 열어 보지 않으니 캡슐화가 지켜진다.

코드로 보는 최소 구현

자바에서는 메멘토를 오리지네이터의 중첩 클래스로 두고 생성자를 private으로 막는 방식이 자주 쓰인다. 이렇게 하면 케어테이커는 메멘토 타입을 들고 다닐 수는 있어도 그 내부 값을 만들거나 읽지는 못한다.

class Editor {    // 오리지네이터
 private String text = "";
 void type(String words) { text += words; }
 String read() { return text; }

 Snapshot save() { return new Snapshot(text); }
 void restore(Snapshot s) { this.text = s.state; }

 static class Snapshot { // 메멘토
  private final String state;
  private Snapshot(String state) { this.state = state; }
 }
}

class History {    // 케어테이커
 private final Deque<Editor.Snapshot> stack = new ArrayDeque<>();
 void push(Editor.Snapshot s) { stack.push(s); }
 Editor.Snapshot pop() { return stack.pop(); }
}

실제로 쓸 때는 상태를 바꾸기 직전에 저장해 두고, 되돌릴 때 마지막 스냅샷을 꺼낸다.

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

editor.type("안녕");
history.push(editor.save()); // 여기까지 저장
editor.type(" 세계");
editor.restore(history.pop()); // read() 는 다시 "안녕"

History는 Snapshot을 보관만 할 뿐 state에는 손대지 못한다. 편집기의 내부 구조가 바뀌어도 History 코드는 그대로 둔다. 이것이 캡슐화를 지킨다는 말의 실제 모습이다. 참고로 자바처럼 중첩 클래스의 접근 제어가 없는 언어라면, 오리지네이터에는 넓은 인터페이스를, 케어테이커에는 값을 못 읽는 좁은 인터페이스를 주는 방식으로 같은 효과를 낸다.

언제 메멘토 패턴이 맞나

되돌릴 상태가 명확하고, 그 상태를 외부에 노출하고 싶지 않을 때 맞는다. 실행 취소·재실행, 편집 이력, 게임 세이브처럼 "이전 시점으로 돌아가기"가 필요한 기능이 전형적이다. 위키백과의 메멘토 패턴 설명도 활용 예로 실행 취소, 버전 관리, 직렬화를 꼽는다.

대가도 있다. 리팩토링 구루는 스냅샷을 자주 만들면 메모리를 많이 쓸 수 있고, 케어테이커가 오래된 메멘토를 정리하도록 수명을 관리해야 한다고 짚는다. 상태 전체를 통째로 복사하는 구조라, 큰 객체를 매 입력마다 저장하면 부담이 빠르게 커진다.

대안도 알아 두면 좋다. 작업 하나하나를 되돌리는 게 더 자연스러우면, 상태 전체 대신 "역동작"을 기록하는 커맨드 패턴이 메모리에 유리할 때가 많다. 반대로 상태를 파일이나 네트워크로 넘겨야 하면 직렬화가 더 맞는다. 메멘토는 그 중간, 즉 한 객체의 상태를 메모리 안에서 캡슐화를 지키며 되돌릴 때 가장 잘 들어맞는다.

더 많은 글 보기 RSS 구독