반응형
Notice
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
Tags
- 인공지능
- 데이터구조
- 버전관리
- 디자인패턴
- 네트워크보안
- 컴퓨터공학
- 보안
- 컴퓨터비전
- 알고리즘
- 프로그래밍언어
- 빅데이터
- Yes
- 파이썬
- 자료구조
- 자바스크립트
- 데이터베이스
- 소프트웨어
- 데이터과학
- 사이버보안
- 클라우드컴퓨팅
- 프로그래밍
- 네트워크
- 컴퓨터과학
- springboot
- 딥러닝
- I'm Sorry
- 데이터분석
- 머신러닝
- 웹개발
- 소프트웨어공학
Archives
- Today
- Total
스택큐힙리스트
메멘토(Memento) 패턴 완전 분석: “되돌리기(Undo)”를 가장 깔끔하게 본문
반응형
복잡한 편집기나 결제 플로우에서 “방금 전으로 돌아가줘”는 거의 필수 기능이죠. 이걸 매번 if/else로 땜빵하다 보면 상태 관리가 산으로 갑니다. 메멘토 패턴은 객체의 내부 구현을 까지 않고(캡슐화 유지) 이전 상태 스냅샷을 저장·복원하게 해 주는 정석 해법이에요.
왜 메멘토인가? (문제 → 해법)
- 문제: 객체가 복잡해질수록 “원래대로” 돌리는 로직이 여기저기 흩어짐 → 사이드이펙트·버그 증가
- 해법: 현재 상태를 Memento(스냅샷) 로 외부에 맡겨 두고, 필요할 때 Originator 가 그 스냅샷으로 복원. 스냅샷 묶음은 Caretaker 가 관리. 구조가 단순하고 테스트가 쉬워집니다.
구성 요소 한 번에 이해하기
- Originator: “상태 주인공”. 스냅샷을 만들고(save()), 나중에 되돌리는(restore()) 역할.
- Memento: 스냅샷 그 자체. 보통 불변(immutable) 으로 설계.
- Caretaker: 스냅샷 보관·관리(스택/큐/DB). Undo/Redo 스택도 여기서 처리.
언제 쓰면 베스트인가
- 에디터 Undo/Redo: 텍스트/이미지/도면 편집기의 기본 UX.
- 상태 머신 롤백: 다단계 가입·결제 플로우에서 오류 시 이전 단계로 복귀.
- 게임 저장·로드: 체크포인트마다 스냅샷 저장, 로드로 즉시 복원.
- 알림·예약 취소: “N초 안엔 취소 가능” 같은 임시 상태 보관.
Kotlin 미니 예제 – 텍스트 에디터 Undo/Redo
// --- Memento ---
@JvmInline value class EditorState(val text: String)
// --- Originator ---
class TextEditor {
var content: String = ""
private set
fun write(s: String) { content += s }
fun save(): EditorState = EditorState(content)
fun restore(m: EditorState) { content = m.text }
}
// --- Caretaker ---
class History {
private val undo = ArrayDeque<EditorState>()
private val redo = ArrayDeque<EditorState>()
fun push(s: EditorState) { undo.addLast(s); redo.clear() }
fun canUndo() = undo.isNotEmpty()
fun canRedo() = redo.isNotEmpty()
fun undo(current: EditorState): EditorState? =
if (undo.isEmpty()) null
else undo.removeLast().also { redo.addLast(current) }
fun redo(current: EditorState): EditorState? =
if (redo.isEmpty()) null
else redo.removeLast().also { undo.addLast(current) }
}
// --- 사용 ---
val editor = TextEditor()
val history = History()
editor.write("Hello, "); history.push(editor.save())
editor.write("World!"); history.push(editor.save())
val prev = history.undo(editor.save())
if (prev != null) editor.restore(prev) // "Hello, "
핵심은 복원 로직이 오직 restore() 한 줄이라는 것. 여기저기 뒤엉킨 롤백 코드를 걷어낼 수 있어요.
실무에서 바로 쓰는 팁 (안드로이드·백엔드 공통)
1) 스냅샷 최소화 & 깊은 복사
스냅샷에 꼭 필요한 필드만 담으세요(재생 위치, 선택된 언어 등). 참조 타입은 깊은 복사를 고려하지 않으면 복원 후 값이 바뀌는 유령 버그가 생깁니다.
2) 보관 전략(메모리 vs. 영속화)
- 가벼운 편집: ArrayDeque 스택만으로 OK.
- 큰 상태/장시간 작업: Room/파일/직렬화로 오프로드. (Refactoring.Guru 한국어 문서도 직렬화를 일반적 방법으로 언급)
3) Undo/Redo UX 규칙
- 사용자 행위 단위에 맞춰 저장(입력 30자마다 X, “문단 완료”나 onPause 시점 O).
- Redo 무효화: Undo 뒤 편집이 발생하면 Redo 스택은 비웁니다(편집기 표준 UX).
4) Command 패턴과 혼동 금지
- Command: “행위를 객체화”해서 실행/취소를 기록(redo/undo 가능).
- Memento: “상태 스냅샷”을 저장·복원. 둘을 함께 쓰기도 합니다(커맨드 실행 전 스냅샷 저장).
5) 테스트 전략
- 단위 테스트: 여러 번의 write() 후 undo() → 기대 텍스트를 정확히 검증.
- 회귀 테스트: 깊은 복사 누락 케이스(리스트/맵 포함)를 따로 만든다.
@Test
fun `undo restores previous snapshot`() {
val e = TextEditor(); val h = History()
e.write("A"); h.push(e.save())
e.write("B"); h.push(e.save())
val prev = h.undo(e.save())
assertEquals("A", prev?.text)
}
장단점 총정리
- 장점
- 캡슐화 유지: 내부 구현을 외부에 노출하지 않고 복원 가능.
- 단순한 롤백: 복원은 restore() 한 번으로 끝.
- 단점
- 메모리 사용: 스냅샷이 크거나 잦으면 용량 부담.
- 깊은 복사 비용: 안전한 스냅샷을 위한 추가 비용이 들 수 있음.
반응형
'개발' 카테고리의 다른 글
Interpreter 패턴: 미니 DSL로 “규칙을 읽는” 코드 만들기 (3) | 2025.08.08 |
---|---|
커맨드 vs 메멘토: 언제 어떤 패턴이 덜 아픈가 (3) | 2025.08.08 |
Visitor 패턴: “구조는 그대로, 기능은 덧붙여” (3) | 2025.07.30 |
이어보기 완성! Iterator + Memento로 구현한 플레이백 히스토리 (1) | 2025.07.30 |
Memento 패턴: “되돌리기(Undo)”를 코드로 구현하는 가장 깔끔한 방법 (0) | 2025.07.30 |
Comments