|

코틀린 디자인 패턴 (18) – Memento 패턴으로 상태 저장과 복원하기

코틀린 디자인 패턴 (18) – Memento 패턴으로 상태 저장과 복원하기
Memento 패턴은 상태를 저장하되 객체의 내부 구현을 외부에 드러내지 않게 돕는다

코틀린 Memento 패턴은 undo, draft 복원, 상태 되돌리기처럼 사용자는 자주 원하지만 구현은 의외로 헷갈리는 기능을 설명할 때 매우 좋은 패턴입니다. 특히 상태를 저장하고 싶지만 캡슐화는 깨고 싶지 않은 상황을 다룬다는 점이 핵심입니다.

이번 글에서는 Memento 패턴을 단순 정의로 보지 않고, 왜 편집기나 폼 상태 복원 같은 장면에서 자주 등장하는지, 코틀린에서는 data class snapshot과 immutable state 감각으로 어떻게 더 읽기 쉽게 풀 수 있는지까지 같이 정리하겠습니다.

Memento 패턴 핵심 카드
이번 글에서 먼저 잡아야 할 핵심 판단 기준

기본 개념 참고로는 Refactoring.Guru의 Memento 패턴 설명, 코틀린 구현 감각으로는 Kotlin data classes 문서를 참고했습니다. 이번 글은 그 내용을 코틀린 상태 모델링 감각으로 다시 풀어 설명합니다.

왜 이 패턴이 필요할까

가장 대표적인 상황은 undo입니다. 텍스트를 수정하거나 폼 값을 바꾸기 전에 이전 상태를 저장해 두고, 사용자가 취소를 누르면 그때 상태를 되돌리고 싶어집니다.

문제는 이 상태를 외부에서 마구 꺼내 읽게 만들면 캡슐화가 깨진다는 점입니다. 내부 필드를 전부 공개하면 snapshot은 만들기 쉬워지지만, 이후 구조가 바뀔 때 외부 코드가 모두 같이 흔들리게 됩니다.

Memento 패턴은 이 문제를 우회합니다. 상태를 가장 잘 아는 객체가 자기 상태를 저장 가능한 형태로 만들고, 외부는 그 저장물 자체를 들고 있다가 필요할 때 복원만 요청하는 흐름입니다.


대표 예시: 편집기와 폼 상태 복원

이 패턴이 자주 편집기 예시와 함께 설명되는 이유는 직관적이기 때문입니다. 문서를 수정하기 전 snapshot을 하나 저장하고, undo를 누르면 가장 최근 snapshot으로 되돌리면 됩니다.

실무적으로는 필터 패널, 폼 입력 draft, 단계형 설정 화면에서도 비슷한 구조가 나옵니다. 사용자가 임시로 상태를 바꾸다가 취소하면 이전 스냅샷을 복원해야 하는 장면입니다.

Memento 패턴 흐름 도식
변경 전 snapshot을 저장하고, 필요할 때 history에서 꺼내 복원하는 흐름

코틀린 코드로 보면 어떻게 생길까

data class EditorMemento(
    val text: String,
    val cursor: Int,
)

class Editor(
    private var text: String,
    private var cursor: Int,
) {
    fun save(): EditorMemento = EditorMemento(text, cursor)

    fun restore(memento: EditorMemento) {
        text = memento.text
        cursor = memento.cursor
    }

    fun update(newText: String, newCursor: Int) {
        text = newText
        cursor = newCursor
    }
}

class History {
    private val stack = ArrayDeque<EditorMemento>()

    fun push(memento: EditorMemento) {
        stack.addLast(memento)
    }

    fun pop(): EditorMemento? =
        if (stack.isEmpty()) null else stack.removeLast()
}

이 예제에서는 Editor가 자기 상태를 save()로 snapshot화하고, History는 그 snapshot을 저장만 합니다. 즉 History는 내부 구조를 해석하지 않고, Editor만 restore를 통해 자기 상태를 다시 되돌립니다.

코틀린에서는 이런 구조를 data class로 비교적 읽기 쉽게 만들 수 있습니다. 다만 data class copy는 얕은 복사라는 점은 꼭 기억해야 합니다. 상태 안에 mutable 컬렉션이나 참조형 객체가 깊게 들어 있으면, 단순 copy만으로는 완전한 독립 snapshot이 안 될 수 있습니다.


Command 패턴과는 무엇이 다를까

undo를 다룬다는 점 때문에 Memento와 Command는 자주 같이 언급됩니다. 하지만 초점이 다릅니다. Command는 요청 자체를 객체로 만들고, Memento는 상태 snapshot을 저장하는 쪽에 더 가깝습니다.

즉 Command는 “무슨 행동을 실행할까”에 가깝고, Memento는 “이전 상태를 어떻게 복원할까”에 가깝습니다. 그래서 undo를 명령 역연산으로 풀 수도 있고, snapshot 복원으로 풀 수도 있습니다.

Memento와 Command 비교 카드
둘 다 undo와 연결되지만 관리하는 핵심 대상이 다르다

이 비교는 앞선 Command 패턴 글과도 연결됩니다. 상태 자체를 저장할지, 행동을 객체로 저장할지는 undo 요구사항의 성격에 따라 달라질 수 있습니다.


코틀린다운 포인트는 무엇일까

코틀린에서는 전통적인 패턴 설명보다 더 가볍게 이 구조를 풀 수 있습니다. immutable state + data class snapshot + history stack 조합만으로도 핵심 의도를 잘 드러낼 수 있기 때문입니다.

특히 UI 상태가 data class로 관리되는 경우라면 이전 상태를 통째로 저장하는 구조가 더 자연스럽게 읽힐 수 있습니다. 그래서 Memento 패턴은 오래된 패턴이지만, 코틀린에서는 오히려 더 직관적으로 보일 때가 있습니다.

다만 상태가 커질수록 snapshot 비용도 함께 커집니다. 예를 들어 대형 텍스트 문서, 이미지 편집 상태, 큰 컬렉션이 포함된 객체는 매번 전체를 복사하는 비용이 부담이 될 수 있습니다. 이때는 diff 저장이나 부분 복원처럼 다른 전략을 검토해야 합니다.


언제 잘 맞고, 언제 과할까

Memento는 사용자가 “되돌리기”를 기대하는 기능에서 특히 잘 맞습니다. 편집기, 폼 draft, 설정 화면, 필터 변경 취소처럼 이전 상태 복원이 UX 핵심일 때 가치가 큽니다.

하지만 상태가 너무 자주 바뀌고 snapshot이 너무 무거우면 오히려 저장 비용과 history 관리 비용이 커집니다. 결국 사용 경험을 좋게 하려다 메모리와 복잡도가 동시에 늘어날 수 있습니다.

Memento가 과해지는 경우 비교 카드
잘 맞는 경우와 과한 경우를 같이 봐야 현실적인 판단이 가능하다

실전에서 스스로 던질 질문

  1. 사용자가 이전 상태 복원을 실제로 자주 기대하는가
  2. snapshot 크기가 감당 가능한 수준인가
  3. 외부가 내부 필드를 직접 만지지 않고도 복원 구조를 유지할 수 있는가
  4. 행동 기반 undo보다 상태 기반 undo가 더 자연스러운가

이 질문에 yes가 많아질수록 Memento 패턴 후보일 가능성이 높습니다. 반대로 복원보다 단순 재조회/재계산이 더 쉬운 구조라면 굳이 snapshot history를 둘 이유가 약해질 수 있습니다.


마무리

Memento 패턴의 핵심은 snapshot 객체를 하나 더 만드는 데 있지 않습니다. 객체 상태를 저장하고 복원해야 할 때, 그 상태의 소유권과 캡슐화를 어떻게 지킬지에 대한 설계 선택에 있습니다.

undo와 상태 복원이 필요하지만 내부 구현은 외부에 드러내고 싶지 않은 상황이 보이면, Memento 패턴은 꽤 실용적인 선택지가 됩니다. 코틀린에서는 data class와 immutable state 감각 덕분에 이 구조를 더 직관적으로 풀 수 있다는 점도 큰 장점입니다.

코틀린 디자인 패턴 시리즈

코틀린 디자인 패턴 시리즈(0) – 왜 아직도 디자인 패턴을 배워야 할까

코틀린 디자인 패턴(1) – Singleton 패턴은 언제 쓰고 object는 어떻게 다를까

코틀린 디자인 패턴(2) – Factory Method 패턴으로 생성 책임 나누기

코틀린 디자인 패턴(3) – Abstract Factory 패턴은 언제 필요할까

코틀린 디자인 패턴(4) – Builder 패턴과 named argument는 어떻게 다를까

코틀린 디자인 패턴(5) – Prototype 패턴과 data class copy 정리

코틀린 디자인 패턴(6) – Adapter 패턴으로 기존 코드를 새 인터페이스에 맞추기

코틀린 디자인 패턴(7) – Bridge 패턴은 상속 폭발을 어떻게 줄일까

코틀린 디자인 패턴(8) – Composite 패턴으로 트리 구조 다루기

코틀린 디자인 패턴(9) – Decorator 패턴은 상속 대신 어떻게 확장할까

코틀린 디자인 패턴(10) – Facade 패턴으로 복잡한 시스템 감추기

코틀린 디자인 패턴(11) – Flyweight 패턴은 메모리를 어떻게 아낄까

코틀린 디자인 패턴(12) – Proxy 패턴은 Decorator와 무엇이 다를까

코틀린 디자인 패턴(13) – Chain of Responsibility 패턴으로 조건 분기 줄이기

코틀린 디자인 패턴(14) – Command 패턴으로 실행과 취소를 다루는 법

코틀린 디자인 패턴(15) – Interpreter 패턴은 어디까지 써야 할까

코틀린 디자인 패턴(16) – Iterator 패턴과 sequence는 어떻게 연결될까

코틀린 디자인 패턴(17) – Mediator 패턴으로 객체 간 얽힘 줄이기

함께보면 좋은 글