메멘토 패턴
객체 내부 상태를 외부에 풀어놓지 않고 스냅샷으로 보관했다가 원본 객체가 복원하는 행위 패턴.
· 마지막 업데이트: 2026-10-25 · 6 min read
개요
편집 전 상태로 돌아가려면 예전 내용을 보관해야 한다. 이력을 관리하는 쪽이 편집기의 내부 필드를 하나씩 읽고 다시 채우면, 편집기의 구조를 바꿀 때 이력 코드도 깨진다.
메멘토(Memento)는 상태를 보관하는 스냅샷이다. 상태를 가진 원본 객체가 저장과 복원을 맡고, 보관자(Caretaker)는 내용을 해석하지 않은 채 스냅샷을 가지고 있는다.
UML
TextEditor가 비공개 Snapshot을 만들고 ISnapshot으로 반환한다. 보관자는 그 핸들을 가지고 있다가 복원 시 돌려준다. 상태를 읽고 쓰는 책임은 TextEditor에 남는다.
역사
GoF는 1994년 Design Patterns에서 Memento를 행위 패턴으로 정리했다. 객체의 캡슐화를 유지하면서 나중에 복원할 상태를 저장하는 방식이다.1
왜 사용하는가?
이력 관리 코드가 편집기의 내부 표현을 알 필요가 없다. 복사할 상태가 늘어나면 원본 객체의 저장·복원 코드에서 함께 관리한다.
스냅샷을 계속 보관하면 메모리를 사용한다. 보관 개수와 수명, 어떤 상태까지 되돌릴지를 정하며 이미 전송한 메일이나 외부 저장소의 변경까지 자동 취소한다고 보지 않는다.
예제
초안을 저장한 뒤 내용을 바꾸고, 저장한 상태로 복원한다.
using System;
using System.Collections.Generic;
using System.Linq;
var editor = new TextEditor();
editor.Write("draft");
ISnapshot saved = editor.Save();
editor.Write("edited");
Console.WriteLine(editor.Restore(saved));
Console.WriteLine(editor.Text);
interface ISnapshot { }
// 복원할 상태의 해석과 변경은 원본 편집기 안에 둔다.
sealed class TextEditor
{
private readonly Guid id = Guid.NewGuid();
private sealed record Snapshot(Guid Owner, string Text) : ISnapshot;
public string Text { get; private set; } = "";
public void Write(string text) => Text = text;
// 외부에는 보관용 토큰만 주고 실제 문자열은 비공개 저장소에 남긴다.
public ISnapshot Save() => new Snapshot(id, Text);
public bool Restore(ISnapshot snapshot)
{
if (snapshot is not Snapshot saved || saved.Owner != id) return false;
Text = saved.Text;
return true;
}
}
True
draft호출부는 ISnapshot에서 문자열을 꺼내지 않는다. TextEditor가 자기 스냅샷을 받아 상태를 돌린다.
범위 넓힌 예제
다른 편집기에 같은 스냅샷을 전달하면 거절하도록 원본 식별자를 검사한다.
using System;
using System.Collections.Generic;
using System.Linq;
var editor = new TextEditor();
editor.Write("draft");
ISnapshot saved = editor.Save();
var other = new TextEditor();
other.Write("other");
Console.WriteLine(other.Restore(saved));
Console.WriteLine(other.Text);
editor.Write("edited");
Console.WriteLine(editor.Restore(saved));
Console.WriteLine(editor.Text);
interface ISnapshot { }
// 복원할 상태의 해석과 변경은 원본 편집기 안에 둔다.
sealed class TextEditor
{
private readonly Guid id = Guid.NewGuid();
private sealed record Snapshot(Guid Owner, string Text) : ISnapshot;
public string Text { get; private set; } = "";
public void Write(string text) => Text = text;
// 외부에는 보관용 토큰만 주고 실제 문자열은 비공개 저장소에 남긴다.
public ISnapshot Save() => new Snapshot(id, Text);
public bool Restore(ISnapshot snapshot)
{
if (snapshot is not Snapshot saved || saved.Owner != id) return false;
Text = saved.Text;
return true;
}
}
False
other
True
draft잘못된 원본의 스냅샷은 false를 반환하고 수신 편집기를 바꾸지 않는다. 예제는 문자열 하나의 메모리 상태만 복원하며, 스냅샷 형식의 파일 저장이나 버전 호환은 별도 문제다.
TypeScript 예제
이력 관리자는 스냅샷을 보관하고, TextEditor만 그 스냅샷의 내용과 복원 방법을 안다. 과거 상태를 보관하려고 편집기의 내부 문자열을 외부에 풀어놓지 않도록 역할을 나눈다.
type ISnapshot = Readonly<{ kind: "snapshot" }>;
// 복원할 상태의 해석과 변경은 원본 편집기 안에 둔다.
class TextEditor {
#text = "";
readonly #snapshots = new WeakMap<ISnapshot, string>();
get text(): string { return this.#text; }
write(text: string): void { this.#text = text; }
// 외부에는 보관용 토큰만 주고 실제 문자열은 비공개 저장소에 남긴다.
save(): ISnapshot {
const token = Object.freeze({ kind: "snapshot" as const });
this.#snapshots.set(token, this.#text);
return token;
}
restore(snapshot: ISnapshot): boolean {
const text = this.#snapshots.get(snapshot);
if (text === undefined) return false;
this.#text = text;
return true;
}
}
const editor = new TextEditor();
editor.write("draft");
const saved = editor.save();
editor.write("new");
console.log(editor.restore(saved));
console.log(editor.text);
console.log(new TextEditor().restore(saved));true
draft
falseTypeScript에서는 불변 토큰과 편집기별 비공개 WeakMap으로 스냅샷 내부를 감췄다. 토큰의 겉모양만 흉내 내거나 다른 편집기의 토큰을 넘겨도 복원되지 않는다.
같이 보기
C# 예제는 각각 독립된 .NET 10 콘솔 프로그램이다.
TypeScript 예제는 ES2022 이상으로 컴파일한 JavaScript를 Node.js에서 실행한다.