되돌릴 수 있는 용기
컴퓨터로 작업하다가 실수했을 때 우리가 가장 먼저 누르는 키는 Ctrl+Z입니다. 문서 편집 중 잘못 삭제한 문장, 포토샵에서 실패한 필터 효과, 코딩 중 망가뜨린 코드.. 모든 것이 이전 상태로 돌아갑니다.
이 "되돌리기" 기능이 없던 시절을 생각해 보죠. 한 번의 실수로 몇 시간의 작업이 날아가고, 조심스럽게 한 글자씩 타이핑해야 했던 그 시절이 있었습니다. 하지만 지금 우리는 자유롭게 실험하고, 시도하고, 실패할 수 있습니다. 언제든 되돌릴 수 있다는 확신이 있기 때문이죠.
Ctrl+Z 기능은 단순해 보이지만 굉장히 정교한 시스템입니다. 프로그램은 우리의 모든 작업을 기억하고 있어야 하고, 필요할 때 정확히 이전 상태로 복원할 수 있어야 합니다. 하지만 모든 상태를 다 저장할 수는 없습니다. 메모리도 한정적이고, 성능도 고려해야 합니다.
이 균형을 이루는 것이 바로 '메멘토 패턴(Memento Pattern)'입니다. 객체의 내부 상태를 외부에 노출하지 않으면서도, 필요할 때 이전 상태로 되돌릴 수 있게 해주는 패턴이죠.
1980년대 후반 GUI 애플리케이션의 급속한 발전으로 당시 사용자들은 점점 더 복잡한 작업을 컴퓨터로 수행하기 시작했고, 실수에 대한 두려움 없이 자유롭게 실험할 수 있는 환경을 원했습니다.
초기 컴퓨터 프로그램들은 대부분 일방향적이었습니다. 한 번 실행된 작업은 되돌릴 수 없었고, 사용자는 매우 조심스럽게 작업해야 했습니다. 하지만 1970년대 후반 Xerox PARC에서 개발된 Bravo 텍스트 에디터가 최초로 "실행 취소(Undo)" 기능을 도입하면서 패러다임이 바뀌기 시작했습니다.
문제는 이런 기능을 구현하는 것이 생각보다 복잡했다는 점입니다. 객체의 상태를 저장하려면 그 객체의 내부 구조를 외부에서 알아야 했는데, 이는 캡슐화 원칙을 위반하죠. 또한 모든 상태를 저장하기에는 메모리 오버헤드가 너무 컸습니다.
1990년대 초, GOF는 객체 자체가 자신의 상태를 "기념품(Memento)"으로 만들어 외부에 제공하고, 나중에 그 기념품을 받아서 상태를 복원하는 아이디어를 고안했습니다. 이때 중요한 것은 기념품 자체는 불변(Immutable)이어야 하고, 오직 원래 객체만이 그 기념품을 해석할 수 있어야 한다는 점이었습니다.
패턴의 이름 "Memento"는 영화 제목이기도 하지만, 원래는 라틴어로 "기억하라"는 뜻입니다. 중세 시대부터 사람들이 중요한 순간을 기억하기 위해 간직하는 기념품을 의미했습니다.
텍스트 에디터를 예제로 들어보겠습니다.
// 메멘토 클래스 (텍스트 상태 저장)
class TextMemento {
private final String content;
private final int cursorPosition;
private final long timestamp;
public TextMemento(String content, int cursorPosition) {
this.content = content;
this.cursorPosition = cursorPosition;
this.timestamp = System.currentTimeMillis();
}
// 패키지 접근 제한으로 TextEditor만 접근 가능
String getContent() { return content; }
int getCursorPosition() { return cursorPosition; }
long getTimestamp() { return timestamp; }
@Override
public String toString() {
return String.format("저장시점: %tT, 내용: \"%.20s%s\"",
timestamp, content, content.length() > 20 ? "..." : "");
}
}
// 원본 객체 (텍스트 에디터)
class TextEditor {
private StringBuilder content;
private int cursorPosition;
public TextEditor() {
this.content = new StringBuilder();
this.cursorPosition = 0;
}
// 텍스트 편집 기능들
public void write(String text) {
content.insert(cursorPosition, text);
cursorPosition += text.length();
System.out.println("✏️ 입력: \"" + text + "\" | 현재 내용: \"" + content + "\"");
}
public void delete(int length) {
if (cursorPosition >= length) {
String deleted = content.substring(cursorPosition - length, cursorPosition);
content.delete(cursorPosition - length, cursorPosition);
cursorPosition -= length;
System.out.println("�️ 삭제: \"" + deleted + "\" | 현재 내용: \"" + content + "\"");
}
}
public void moveCursor(int position) {
if (position >= 0 && position <= content.length()) {
cursorPosition = position;
System.out.println("� 커서 이동: " + position + "번째 위치");
}
}
// 메멘토 생성 (현재 상태 저장)
public TextMemento createMemento() {
return new TextMemento(content.toString(), cursorPosition);
}
// 메멘토로부터 상태 복원
public void restoreFromMemento(TextMemento memento) {
this.content = new StringBuilder(memento.getContent());
this.cursorPosition = memento.getCursorPosition();
System.out.println("⏪ 상태 복원: \"" + content + "\" (커서: " + cursorPosition + ")");
}
public String getContent() { return content.toString(); }
public int getCursorPosition() { return cursorPosition; }
}
// 관리자 (여러 메멘토 관리)
class EditorHistory {
private Stack<TextMemento> history = new Stack<>();
private int maxHistorySize = 10;
public void save(TextEditor editor) {
// 히스토리 크기 제한
if (history.size() >= maxHistorySize) {
history.remove(0); // 가장 오래된 것 제거
}
TextMemento memento = editor.createMemento();
history.push(memento);
System.out.println("� 상태 저장됨 (" + history.size() + "/" + maxHistorySize + ")");
}
public boolean undo(TextEditor editor) {
if (!history.isEmpty()) {
TextMemento memento = history.pop();
editor.restoreFromMemento(memento);
System.out.println("↩️ 실행 취소 완료");
return true;
} else {
System.out.println("❌ 더 이상 되돌릴 수 없습니다");
return false;
}
}
public void showHistory() {
System.out.println("\n� 편집 히스토리:");
if (history.isEmpty()) {
System.out.println(" (비어있음)");
} else {
for (int i = history.size() - 1; i >= 0; i--) {
System.out.println(" " + (i + 1) + ". " + history.get(i));
}
}
}
}
// 텍스트 에디터 데모
public class TextEditorDemo {
public static void main(String[] args) {
TextEditor editor = new TextEditor();
EditorHistory history = new EditorHistory();
System.out.println("� 텍스트 에디터 시작!");
System.out.println("=".repeat(50));
// 편집 작업과 상태 저장
System.out.println("\n1️⃣ 첫 번째 문장 작성");
editor.write("안녕하세요");
history.save(editor);
System.out.println("\n2️⃣ 두 번째 문장 추가");
editor.write(", 반갑습니다");
history.save(editor);
System.out.println("\n3️⃣ 문장 중간에 삽입");
editor.moveCursor(5); // "안녕하세요" 다음
editor.write(" 여러분");
history.save(editor);
System.out.println("\n4️⃣ 일부 삭제");
editor.moveCursor(editor.getContent().length()); // 끝으로 이동
editor.delete(4); // "습니다" 삭제
history.save(editor);
System.out.println("\n5️⃣ 추가 입력");
editor.write("요!");
// 히스토리 확인
history.showHistory();
// 실행 취소 테스트
System.out.println("\n" + "=".repeat(50));
System.out.println("� 실행 취소 테스트");
System.out.println("=".repeat(50));
System.out.println("\n현재 내용: \"" + editor.getContent() + "\"");
// 여러 번 되돌리기
for (int i = 1; i <= 5; i++) {
System.out.println("\n⏪ " + i + "번째 되돌리기:");
if (!history.undo(editor)) {
break;
}
}
System.out.println("\n" + "=".repeat(50));
System.out.println("✅ 텍스트 에디터 데모 완료!");
}
}
위 코드에서 가장 핵심적인 부분은 `TextMemento` 클래스입니다. 이 클래스는 텍스트 에디터의 특정 시점 상태를 "캡슐화"합니다.
`TextMemento`의 필드들은 private이고, getter 메서드들은 패키지 접근 제한자를 사용합니다. 이는 오직 같은 패키지의 `TextEditor`만이 메멘토의 내용을 읽을 수 있다는 뜻입니다. 외부에서는 메멘토를 저장하고 전달할 수는 있지만, 내용을 들여다볼 수는 없습니다.
메멘토는 한 번 생성되면 변경될 수 없습니다. 모든 필드가 final이고, 문자열과 기본 타입만 저장합니다. 이는 메멘토가 생성된 후 원본 객체가 변경되어도 메멘토는 영향받지 않음을 보장합니다.
`EditorHistory`는 메멘토들을 스택으로 관리하며, 최대 크기를 제한합니다. 이는 메모리 사용량을 제어하면서도 충분한 되돌리기 기능을 제공합니다.
사용자 관점에서 되돌리기는 마법 같지만, 실제로는 저장된 메멘토에서 상태를 읽어와 객체를 정확히 복원하는 과정입니다. 복원 후에는 마치 시간이 되돌아간 것처럼 모든 것이 이전 상태 그대로입니다.
인생이 Ctrl+Z같이 이전 상태로 되돌릴 수 없습니다. 메멘토가 ‘기억하라’라는 의미와 같이 되돌릴 수는 없지만, 경험을 기억할 수 있습니다. 원상태로 되돌리기보다는 실패와 실수를 통해 경험을 기억하면 더 나은 발전방향으로 나아갈 수 있습니다.