도형 편집기를 만든다고 하자. 사용자가 사각형을 드래그해 옮겼는데 마음에 안 들어 Ctrl+Z를 누른다. 그러면 사각형은 옮기기 직전의 위치로 되돌아가야 한다. 실행 취소(undo)를 지원하려면, 조작을 하기 직전에 그 객체의 상태를 어딘가 찍어 두었다가 취소 명령이 오면 그 지점으로 되돌려야 한다. 문제는 “상태를 찍어 둔다”를 어떻게 구현하느냐다.

가장 손쉬운 길은 도형의 좌표·크기·각도를 게터로 다 꺼내 어딘가에 담아 두고, 되돌릴 때 세터로 도로 밀어 넣는 것이다. 동작은 한다. 그러나 이 순간 도형의 내부 필드가 전부 바깥으로 공개된다. undo를 위해 열어 둔 게터·세터를, 다른 코드가 아무 때나 호출해 도형의 속을 헤집을 수 있게 된 것이다. 실행 취소라는 기능 하나를 얻으려고 객체의 캡슐화 전체를 팔아 버린 셈이다. 메멘토(Memento) 패턴은 바로 이 거래를 거부한다 — 상태는 저장하되, 내부는 열지 않는다.

무엇이 문제인가 — 동기

undo를 순진하게 구현하면 두 갈래로 아프다. 둘 다 결국 “상태에 접근하려면 내부를 열어야 한다”는 데서 나온다.

첫째, 상태를 꺼내려고 게터를 다 열면 캡슐화가 무너진다. 도형이 위치를 x, y로 들고 있든 극좌표로 들고 있든, 그건 도형의 사정이어야 한다. 그런데 undo를 위해 getX(), getY(), getAngle(), setX(), setY()…를 전부 공개하면, 이제 도형의 표현 방식이 공개 API가 되어 버린다. 나중에 좌표를 다르게 저장하도록 도형을 리팩터링하려는데, undo 코드가 그 게터·세터에 매달려 있어 함부로 못 바꾼다. 내부 표현을 숨기는 것이 캡슐화인데, undo가 그 벽에 구멍을 뚫었다.

둘째, 상태를 누가 들고 있어야 하는지가 애매하다. undo를 관리하는 쪽(실행 취소 스택을 쥔 객체)이 상태를 보관해야 하는데, 그 관리자가 도형의 상태를 의미까지 알고 보관하면, 관리자가 도형의 내부 구조에 결합된다. “이 스냅샷의 세 번째 값이 각도”라는 걸 관리자가 알아야 한다면, 도형이 필드를 하나 추가할 때마다 관리자도 바뀐다. 상태를 보관하는 책임과 상태를 해석하는 책임이 뒤엉키는 것이다.

정리하면 딜레마는 이렇다. 상태를 저장하려면 객체 밖으로 꺼내야 하는데, 밖으로 꺼내는 순간 그 상태가 아무나 볼 수 있게 되거나, 보관하는 쪽이 그 의미에 결합된다. 저장과 캡슐화가 정면으로 충돌한다.

판단 기준: “이 객체의 상태를 나중에 되돌려야 하는데, 그 상태의 내부는 남에게 보이면 안 된다”는 두 요구가 동시에 있으면 메멘토를 의심한다. 함정: undo가 필요 없거나, 상태가 이미 공개된 값 몇 개뿐이라면 메멘토는 과하다 — 지킬 캡슐화가 없는데 봉인 봉투부터 만드는 꼴이다.

패턴의 구조 — 누가 무엇을 하나

메멘토는 세 배역으로 이 딜레마를 푼다. 핵심은 누가 내부를 볼 수 있고 누가 볼 수 없는가를 배역별로 다르게 준 것이다.

Originator(원본). 상태를 가진 당사자, 위 예의 도형이다. 자기 상태의 스냅샷을 스스로 만들고(createMemento), 스냅샷을 받아 자기를 그 시점으로 되돌린다(restore). 상태를 꺼내고 되돌리는 일을 자기가 하므로, 게터·세터를 바깥에 열 필요가 없다. 스냅샷을 만드는 것도 읽는 것도 전부 Originator 안에서 일어난다.

Memento(메멘토). Originator의 상태를 담은 불투명한 꾸러미다. 핵심은 이것이 두 얼굴을 가진다는 점이다. Originator에게는 속을 다 보여 주지만(상태를 넣고 뺄 수 있어야 하니까), 다른 누구에게도 속을 열지 않는다. 바깥에서 보면 Memento는 아무 메서드도 없는 봉인된 봉투다 — 안에 뭐가 들었는지 물을 수조차 없다. 이 “넓은 인터페이스(Originator용)와 좁은 인터페이스(나머지용)“의 분리가 메멘토 패턴의 전부라 해도 좋다.

Caretaker(관리자). 메멘토를 보관만 하는 쪽, undo 스택을 쥔 객체다. 결정적인 제약이 여기 있다 — Caretaker는 Memento를 들고 있지만 절대 열어 보지 않는다. 언제 스냅샷을 찍을지, 언제 어느 스냅샷으로 되돌릴지는 관리하되, 그 안에 든 상태가 무엇인지는 모른다. Caretaker에게 Memento는 그저 “언젠가 Originator에게 도로 건네줄 검은 상자”일 뿐이다.

협력은 이렇게 흐른다. 조작 직전, Caretaker가 Originator에게 “스냅샷을 다오” 한다. Originator가 자기 상태를 Memento에 봉해 건넨다. Caretaker는 그 봉투를 스택에 쌓아 둔다. undo가 오면, Caretaker가 스택에서 봉투를 꺼내 Originator에게 “이걸로 되돌려라” 하고 도로 건넨다. Originator가 봉투를 열어 자기를 그 상태로 복원한다. 봉투를 여는 것은 오직 만든 자(Originator)뿐이고, 다른 자(Caretaker)는 운반만 한다.

판단 기준: 스냅샷을 만들고 여는 일은 상태의 주인(Originator)에게, 보관하고 순서를 정하는 일은 관리자(Caretaker)에게 갈라 준다. 함정: Caretaker가 Memento의 속을 들여다보기 시작하면 패턴이 무너진다 — 그 순간 상태의 의미가 관리자로 새어 나가 캡슐화가 다시 깨진다.

코드로 — 패턴이 자라나는 과정

내부를 게터로 다 여는 순진한 undo에서 출발해, 상태를 불투명한 메멘토로 봉하고, 만든 자만 열 수 있게 좁혀 가는 과정을 단계로 따라간다.

Refactoring Step 내부를 다 여는 순진한 undo — 캡슐화가 뚫렸다
1/5
public class Shape {    private int x, y;          // 위치    private int angle;         // 회전각    public void moveTo(int x, int y) { this.x = x; this.y = y; }    public void rotate(int a)        { this.angle += a; }    // undo를 위해 내부를 전부 공개한다 — 여기가 화근이다.    public int getX() { return x; }    public int getY() { return y; }    public int getAngle() { return angle; }    public void setX(int x) { this.x = x; }    public void setY(int y) { this.y = y; }    public void setAngle(int a) { this.angle = a; }}// 실행 취소 관리자가 상태를 직접 꺼내 필드로 보관한다.public class History {    private int savedX, savedY, savedAngle;   // Shape의 내부 구조를 그대로 안다    public void save(Shape s) {        savedX = s.getX();  savedY = s.getY();  savedAngle = s.getAngle();    }    public void undo(Shape s) {        s.setX(savedX);  s.setY(savedY);  s.setAngle(savedAngle);    }}// 두 가지가 동시에 아프다. ① Shape의 모든 필드가 public getter/setter로 노출돼// 아무나 속을 헤집을 수 있다. ② History가 "Shape는 x·y·angle로 이뤄진다"는// 내부 구조를 그대로 안다 — Shape에 필드가 하나 늘면 History도 바뀐다.

첫 스텝의 죄는 “상태를 저장한다”를 “내부를 공개한다”와 같은 것으로 취급한 데 있다. 메멘토는 이 둘을 갈라 놓는다. 상태는 여전히 객체 밖(History의 스택)으로 나가지만, 읽을 수 없는 봉투에 담겨 나간다. 그래서 Shape는 게터를 하나도 열지 않고, History는 Shape의 필드가 무엇인지 영영 모른 채로도 undo가 완벽히 동작한다. 자바에서 이 “두 얼굴”은 대개 패키지 범위 접근이나 중첩 클래스로 구현한다 — Memento의 속을 Originator에게만 열고 나머지에는 닫는 언어적 장치다. (C++의 friend 선언, 자바의 private 중첩 클래스가 이 자리에서 쓰인다.)

판단 기준: 리팩터링 후 “Caretaker가 Memento의 상태에 접근할 문법적 수단이 아예 없다”가 성립하면 메멘토가 제대로 앉은 것이다. 함정: Memento에 public 게터를 달아 두면 봉투가 투명해져, undo는 되지만 캡슐화는 처음 그대로 뚫려 있다 — 봉투의 핵심은 “닫혀 있음”이다.

결과 — 무엇을 얻고 무엇을 내주나

메멘토가 사는 값은 캡슐화를 지킨 채로 얻는 undo다. Originator는 상태를 되돌리는 기능을 제공하면서도 내부 필드를 하나도 공개하지 않는다. undo를 위한 게터·세터가 API에 번지지 않으니, 나중에 Originator의 내부 표현을 자유롭게 바꿀 수 있다 — 바깥은 그 변화를 볼 수 없기 때문이다. 그리고 상태 스냅샷이 Originator 밖으로 나가 Caretaker가 관리하므로, undo 스택·리두·체크포인트 같은 이력 관리 로직이 Originator를 어지럽히지 않고 별도 객체로 깔끔히 갈라진다.

대가는 뚜렷하게 세 가지다. 첫째, 메모리·복사 비용이다. 스냅샷은 상태의 사본이다. 도형 몇 개면 가볍지만, 상태가 크거나(문서 전체·이미지 버퍼) undo 이력이 길면 사본이 우수수 쌓여 메모리를 먹는다. 매 조작마다 전체 상태를 통째로 복사하는 것은 비쌀 수 있고, 이걸 줄이려면 변경분만 저장하는 증분(incremental) 방식으로 정교해져야 하는데 그건 그것대로 복잡도를 부른다. 둘째, Originator가 복사 비용을 떠안는다. 상태에 다른 객체 참조가 얽혀 있으면, 얕은 복사로는 나중에 원본이 바뀔 때 스냅샷까지 오염되므로 깊은 복사가 필요해진다 — 무엇을 어디까지 복사할지 결정하는 일이 Originator의 새 책임이 된다. 셋째, Caretaker가 언제 스냅샷을 버릴지 알기 어렵다. Caretaker는 Memento의 속을 모르므로 그 안의 상태가 얼마나 무거운지, 언제 폐기해도 되는지 판단할 근거가 없다. 이력을 무한정 쌓지 않으려면 스택 크기 제한 같은 정책을 따로 둬야 한다.

운영비용 렌즈로 보면 순이득의 조건이 분명하다. undo가 정말 필요하고, 되돌릴 상태가 감춰져야 할 내부라면, 복사 비용을 치르더라도 캡슐화를 지킨 이득이 크다. 반대로 상태가 이미 공개된 몇 개 값뿐이거나 undo가 필요 없다면, 메멘토는 지킬 것 없는 캡슐화를 위해 복사 비용만 무는 간접성이 된다.

판단 기준: 얻는 것(캡슐화 유지·이력 관리 분리)과 내주는 것(메모리·복사 비용·폐기 정책)을 세어 순이득이 양수일 때만 메멘토를 앉힌다. 함정: 상태가 큰데 매 조작마다 전체를 복사하면 메모리가 폭발한다 — 이력이 길어질 자리라면 처음부터 증분 저장이나 스택 상한을 함께 설계해야 한다.

언제 쓰고, 언제 과용인가

메멘토가 정확히 맞는 자리는 이렇다. 실행 취소/재실행이 필요한 편집기·그래픽 도구. 트랜잭션이 실패하면 이전 상태로 되돌려야 하는 롤백. 게임의 세이브 포인트, 마법사(wizard)의 단계별 뒤로 가기, 계산 중간 상태의 체크포인트. 공통점은 하나다 — 어떤 시점의 상태를 나중에 복원해야 하는데, 그 상태의 내부는 남에게 보여선 안 된다. 이 두 요구가 함께일 때가 메멘토의 자리다.

과용의 냄새는 반대편에서 난다. 지킬 캡슐화가 없는데 봉투부터 만드는 경우가 가장 흔하다. 상태가 이미 공개된 값 몇 개라면, 그냥 그 값들을 복사해 두는 것으로 충분하고 Memento라는 배역을 세울 이유가 없다. 상태가 크고 이력이 긴데 전체 복사로 밀어붙이는 것도 과용의 다른 얼굴이다 — 메모리 대가를 계산하지 않고 패턴만 적용한 것이다. 그리고 Memento에 public 게터를 달아 아무나 열게 만들었다면, 문법은 메멘토지만 그 봉투는 이미 뜯겨 있어 패턴의 값이 0이다.

여기서 이웃 패턴과의 경계를 그어 둘 필요가 있다. 메멘토는 [커맨드]와 짝을 이룰 때 진가를 낸다. 커맨드는 “무엇을 했는가”(옮기기·회전)를 객체로 캡슐화하고, 그 커맨드가 실행 직전 자기 대상의 메멘토를 찍어 두면, undo는 “저장된 메멘토로 복원”으로 구현된다 — 커맨드가 실행 취소의 트리거와 순서를, 메멘토가 되돌릴 상태를 각각 맡는 분업이다. 실전의 undo 스택은 대개 이 둘의 합작이다. 한편 [반복자]와는 미묘하게 닮았다 — 반복자도 순회의 현재 위치라는 상태를 별도 객체로 뽑아낸다. 반복자가 “어디까지 봤는가”를 집합체 밖으로 안전하게 들고 다니듯, 메멘토는 “어떤 시점이었는가”를 원본 밖으로 안전하게 들고 다닌다. 둘 다 남의 내부 상태를 캡슐화를 깨지 않고 외부에 보관한다는 발상을 공유한다.

판단 기준: 되돌릴 상태가 있고 그 내부를 숨겨야 하면 메멘토, 되돌릴 ‘조작’을 객체로 다루고 순서·큐·매크로가 필요하면 커맨드 — 대개 둘을 함께 쓴다. 함정: 상태를 저장한다는 이유만으로 무조건 메멘토를 세우면, 공개돼도 무방한 값까지 봉투에 담아 배역만 늘린다 — “이 상태를 정말 숨겨야 하는가”를 먼저 물어야 한다.

다음으로 감시자