Memento 패턴: 캡슐화를 깨지 않고 상태를 저장했다 되돌리는 법
Memento 패턴으로 객체 상태를 캡슐화를 유지한 채 스냅샷하고 복원하는 방법, 스냅샷 전략별 트레이드오프를 Java 예제로 정리한다.
2026-08-13 · 최초 발행 2025-10-14
Undo 기능을 만들 때 가장 쉬운 길은 객체의 내부 상태를 통째로 외부에 노출해 저장하는 것이지만, 그러면 캡슐화가 깨진다. Memento 패턴은 그 캡슐화를 유지한 채로 객체의 내부 상태 스냅샷을 만들고 보관했다가 복원하는 GoF Behavioral 패턴이다. 에디터의 Undo/Redo, 폼 입력 되돌리기, 게임 저장 같은 곳에서 사용자 경험과 운영 안정성을 함께 챙기는 역할을 한다.
캡슐화를 지키면서 상태를 되돌린다는 것
정의는 이렇다 — 객체의 캡슐화를 깨뜨리지 않고 내부 상태 스냅샷을 생성·보관·복원하는 패턴이다. 구성 요소는 Originator, Memento, Caretaker로 나뉜다. Originator는 상태를 보유하고 스냅샷 생성·복원을 수행하는 주체이고, Memento는 내부 상태 스냅샷으로 불변 객체로 설계하길 권장하며, Caretaker는 Memento를 보관하고 수명을 관리할 뿐 내용에는 접근하지 않는다. 목적은 상태 변경의 가역성을 확보하고 사용자 오류·실패 시점에서 빠르게 롤백하며 상태 일관성을 지키는 데 있다.
캡슐화 보존
Memento의 내부 상태는 Originator만 접근하고, Caretaker는 저장·전달 역할만 한다. 불변(Immutable)으로 설계하면 참조 누수와 경쟁 상태를 막을 수 있다.
스냅샷 전략
전체 복사(Full), 얕은 복사(Shallow), 차등 저장(Delta), 직렬화(Serialized) 중에서 고른다. 대상 객체 그래프 크기, 변경 빈도, 성능 목표에 따라 선택이 갈린다.
히스토리 관리
Stack/Deque 기반으로 Undo/Redo를 운영하고, 최대 길이를 제한하며 주기적으로 압축(Compaction)한다. 메모리 임계치에 닿으면 LRU로 폐기하거나 디스크로 오프로딩한다.
복원은 원자적으로
부분 복원은 허용하지 않는다. 실패하면 재시도·롤백 정책을 따르고, 유효성 검증과 상태 일관성 확인 절차를 거친다.
스냅샷 빈도가 오르면 복원 속도는 빨라지지만 메모리 사용량도 함께 오른다. Delta 방식을 쓰면 CPU 오버헤드는 늘고 메모리 사용량은 준다.
저장하고 복원하는 흐름
저장(Save)은 락을 얻고 불변 객체로 스냅샷을 만들어 Caretaker에 전달한 뒤 락을 해제하는 순서다. 스냅샷을 만드는 동안에는 외부 상태 참조를 차단하는 게 좋다(Copy-on-Write 권장). 복원(Restore)은 락을 얻고 최신 Memento를 받아 버전·스키마로 유효성을 검증한 뒤 단일 트랜잭션 안에서 교체하고 락을 해제한다.
히스토리가 비어 있으면 복원 요청을 거부하거나 No-Op으로 처리한다. 스냅샷 버전이나 필드가 달라졌다면 마이그레이션 레이어를 적용하고, 메모리 압력이 있으면 최대 히스토리 길이·스냅샷 크기 상한·오프로딩 정책으로 대응한다.
텍스트 에디터로 보는 구현
전제조건은 JDK 17, 단일 파일로 컴파일할 수 있다.
// File: MementoDemo.java
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Optional;
class TextEditor { // Originator
private String content = "";
public void type(String text) {
content += text;
}
public String getContent() {
return content;
}
public Memento save() { // 스냅샷 생성
return new Memento(content);
}
public void restore(Memento m) { // 원자적 복원
this.content = m.state();
}
// 불변 Memento
public static final class Memento {
private final String state;
private Memento(String state) { this.state = state; }
public String state() { return state; }
}
}
class History { // Caretaker
private final Deque<TextEditor.Memento> undo = new ArrayDeque<>();
public void push(TextEditor.Memento m) {
undo.push(m);
}
public Optional<TextEditor.Memento> pop() {
return Optional.ofNullable(undo.pollFirst());
}
public int size() { return undo.size(); }
}
public class MementoDemo {
public static void main(String[] args) {
TextEditor editor = new TextEditor();
History history = new History();
editor.type("Hello");
history.push(editor.save());
editor.type(", World");
history.push(editor.save());
editor.type("!!!");
System.out.println("현재: " + editor.getContent()); // Hello, World!!!
// Undo 1
history.pop().ifPresent(editor::restore);
System.out.println("복원1: " + editor.getContent()); // Hello, World
// Undo 2
history.pop().ifPresent(editor::restore);
System.out.println("복원2: " + editor.getContent()); // Hello
// 히스토리 없음 처리
if (history.pop().isEmpty()) {
System.out.println("복원 불가: 히스토리 없음");
}
}
}
Memento는 외부에 노출하지 않는다. 직렬화가 필요하면 별도 DTO 변환 계층을 둔다. 히스토리 상한을 설정하고 스냅샷 크기·생성/복원 지연·실패율 같은 지표를 수집한다.
구현 옵션을 비교하면
| 구현 옵션 | 성능(지연) | 확장성(대상 크기) | 일관성(정밀도) | 안정성(오류 격리) | 운영 편의 |
|---|---|---|---|---|---|
| 전체 복사(Full) | 중 | 중 | 높음 | 높음 | 높음 |
| 얕은 복사(Shallow) | 낮음 | 높음 | 중(공유 참조) | 중 | 중 |
| 차등 저장(Delta) | 중~높음 | 높음 | 높음(재조합) | 중 | 중 |
| 직렬화(Serialized) | 중 | 높음(오프로딩) | 높음 | 높음(격리) | 중 |
빈번하게 소규모로 변경된다면 Delta가 유리하고, 단순성과 안정성이 우선이면 Full 복사가 낫다. 대규모 상태를 장기 보관해야 한다면 직렬화와 오프로딩을 함께 쓰는 편이 맞다.
실무에서 만나는 자리
문서·코드 편집기의 Undo/Redo는 타이핑·포맷 변경 단위로 스냅샷을 찍고 최대 단계를 제한하며 배치로 압축한다. 그래픽·게임 엔진은 장면 그래프 스냅샷을 체크포인트로 저장·로드하고 프레임 델타와 병행한다. 폼·워크플로우는 단계 전환 시 스냅샷을 저장해 오류가 나면 이전 단계로 롤백한다. 도메인 모델 변경을 보호할 때는 애그리게이트 변경 전에 스냅샷을 찍어 실패 시 일관성 상태로 복원한다.
보안과 운영에서 신경 쓸 것
민감 정보가 포함된 스냅샷은 암호화·마스킹하고 저장 기간을 제한하며 로그에는 노출하지 않는다. 외부 저장소를 쓴다면 전송 구간을 암호화하고 접근을 통제한다. 동시성은 Originator 단위 락이나 버전드 스냅샷(COW)으로 다루고, 경합이 심한 환경에서는 락 범위를 최소화한다. 히스토리에는 상한·TTL을 두고 메모리 압력에 대응(OOM 방지)하며 백그라운드로 압축한다.
불변 Memento는 안전성을 높이는 대신 생성 비용이 늘고, Delta는 메모리를 아끼는 대신 복원 지연(재조합)이 늘어나는 트레이드오프가 있다. 관측성 측면에서는 스냅샷 크기, 생성/복원 평균 지연, 실패율을 지표화하고, 저장→변경→복원→동등성 검사를 회귀 테스트로 자동화한다.
이렇게 캡슐화를 지키면서 스냅샷·히스토리 관리를 표준화하면 사용자 오류 복구 시간(MTTR)을 5090% 단축한 사례가 있고, 히스토리 상한과 압축을 도입해 메모리 사용량을 3070% 절감한 사례도 있다. 모듈 간 결합도가 줄면서 유지보수성도 함께 오른다.