스택큐힙리스트

Memento 패턴: “되돌리기(Undo)”를 코드로 구현하는 가장 깔끔한 방법 본문

개발

Memento 패턴: “되돌리기(Undo)”를 코드로 구현하는 가장 깔끔한 방법

스택큐힙리스트 2025. 7. 30. 08:30
반응형

복잡한 앱을 만들다 보면 “방금 전 상태로 돌려줘!” 라는 요구가 꼭 생깁니다. 편집기에서의 Undo/Redo, 게임의 저장·로드, 그리고 상태 머신에서 특정 단계로 롤백 하기까지—모두 객체의 이전 상태를 안전하게 보관­-복원하는 Memento 패턴으로 해결할 수 있습니다.
Memento는 캡슐화를 깨지 않고 객체의 스냅샷을 외부에 보관했다가 원할 때 정확히 그 상태로 되돌리는 행위(Behavioral) 패턴입니다.


언제, 왜 써야 할까?

  • 상태 머신 롤백: 복잡한 워크플로에서 오류가 나면 직전 단계로 안전하게 복귀.
  • 알림 시스템 취소: 푸시 예약을 “1분 안에 취소” 하도록 임시 스냅샷 보관.
  • 에디터 Undo/Redo: 사용자가 편집할 때마다 스냅샷을 스택에 쌓았다가 단계별 복원.

핵심은 “상태 이력” 을 Caretaker가 관리하고, 객체 내부 구현은 건드리지 않는다는 점!


3개 핵심 역할만 기억하자

  1. Originator – 현재 상태를 갖고 스냅샷을 만들고 복원하는 주체
  2. Memento – 스냅샷 객체(불변·읽기 전용)
  3. Caretaker – 스냅샷들을 보관하고 필요할 때 Originator에게 돌려줌

Kotlin 실전 예제 – 글쓰기 화면 Undo/Redo

// --- Originator ---
data class EditorState(val text: String)

class TextEditor {
    var content: String = ""
        private set

    fun write(newText: String) { content += newText }
    fun save() = EditorState(content)          // -> Memento
    fun restore(memento: EditorState) {        // <- Memento
        content = memento.text
    }
}

// --- Caretaker ---
class History {
    private val undoStack = ArrayDeque<EditorState>()
    private val redoStack = ArrayDeque<EditorState>()

    fun push(state: EditorState) { undoStack.push(state); redoStack.clear() }
    fun undo(current: EditorState): EditorState? =
        if (undoStack.isEmpty()) null else undoStack.pop().also { redoStack.push(current) }
    fun redo(current: EditorState): EditorState? =
        if (redoStack.isEmpty()) null else redoStack.pop().also { undoStack.push(current) }
}

// --- 사용 예 ---
val editor = TextEditor()
val history = History()

editor.write("Hello, ")
history.push(editor.save())

editor.write("World!")
history.push(editor.save())

history.undo(editor.save())?.let { editor.restore(it) }  // => "Hello, "

이처럼 Originator(TextEditor) 는 자신만 아는 내부 상태를 EditorState 에 담아 Caretaker(History)에 맡깁니다. Caretaker는 스택을 이용해 다단계 Undo/Redo를 구현하고, 텍스트 에디터는 오직 “쓰기·저장·복원” 만 알면 되므로 단일 책임이 지켜집니다.


JUnit 테스트 스케치

@Test
fun `undo restores previous content`() {
    val editor = TextEditor()
    val history = History()
    editor.write("A").also { history.push(editor.save()) }
    editor.write("B").also { history.push(editor.save()) }

    val prev = history.undo(editor.save())
    assertEquals("A", prev?.text)
}

간단하지만 핵심 로직(스냅샷 저장·복원)이 잘 동작하는지 검증할 수 있습니다.


장단점 짚어보기

  • 👍 완벽한 캡슐화: Originator 내부를 외부가 엿보지 않아도 상태 저장 가능.
  • 👍 간단한 복원 로직: 복구 시 side-effect 최소화—그냥 restore() 호출이면 끝.
  • 👎 메모리 부담: 상태가 크거나 스냅샷이 잦으면 Caretaker 메모리 사용량 ↑.
  • 👎 깊은 복사 주의: 얕은 복사로 참조만 넘기면 복원 시 예상치 못한 변경 위험.

한 줄 정리

Memento 패턴은 “캡슐화된 타임머신”—객체를 과거로 안전하게 되돌리고 싶을 때 가장 우아한 해법입니다.

 

반응형
Comments