Memento
Memento 패턴이란?
Memento 패턴은 객체의 상태를 캡처하고 저장하여 나중에 그 상태로 복원할 수 있게 하는 행동 패턴입니다. 주로 객체의 내부 상태를 보호하면서도, 외부에서 객체의 이전 상태로 되돌릴 수 있는 기능을 제공하는데 사용됩니다. 이 패턴은 주로 “되돌리기Undo” 기능이나 객체의 상태 변경 이력을 관리하는 기능을 구현할 때 유용합니다.
Memento 패턴 구조
- Client → Originator
- 클라이언트는
Originator
에게 상태를 설정합니다(Set State()
).
- 클라이언트는
- Originator → Memento
Originator
는 현재 상태를Memento
객체에 저장합니다(Create Memento()
).
- Originator → Caretaker
Originator
는 생성된Memento
를Caretaker
에게 전달하여 저장하게 합니다(Save Memento(memento)
).
- Caretaker → Memento
Caretaker
는 전달받은Memento
객체를 저장소에 보관합니다(Store(memento)
).
- Client → Caretaker
- 클라이언트는 이전 상태로 복원하기 위해
Caretaker
에게Memento
를 요청합니다(Request Memento()
).
- 클라이언트는 이전 상태로 복원하기 위해
- Caretaker → Originator
Caretaker
는 요청받은Memento
객체를Originator
에게 제공합니다(Provide Memento(memento)
).
- Originator → Originator
Originator
는 전달받은Memento
를 사용하여 자신의 상태를 복원합니다(Restore State(memento)
).
Memento 패턴 적용
Memento 패턴의 필요성
도서 관리 시스템에서 사용자의 데이터를 안전하게 보호하면서도, 사용자가 특정 상태로 되돌아가고 싶은 상황이 발생할 수 있습니다. 예를 들어, 사용자가 도서 정보를 수정하다가 이전 상태로 되돌리기를 원할 때 이 패턴이 필요합니다.
잘못된 처리
일반적으로 잘못 처리하는 상황은 객체의 상태를 직접 관리하는 방식입니다. 사용자가 상태를 변경할 때마다 직접적으로 상태를 저장하고, 그 상태를 되돌리기 위한 복잡한 코드를 추가하게 되면 유지 보수성이 떨어집니다.
public class Book
{
public string Title { get; set; }
public string Author { get; set; }
public void ModifyTitle(string newTitle)
{
Title = newTitle;
}
public void UndoTitleChange(string previousTitle)
{
Title = previousTitle;
}
}
직접적인 상태 관리
객체의 상태를 직접 관리하면, 변경 내역을 추적하는 코드가 중복되거나 복잡해질 수 있습니다.
단일 책임 원칙 위반
상태 복원을 위한 로직이 객체 내부에 추가되어, 객체의 책임이 불필요하게 많아집니다. 이는 단일 책임 원칙을 위반하게 됩니다.
Memento 패턴 적용 예시
Memento 패턴을 적용하면 객체의 상태를 안전하게 저장하고, 필요한 시점에 그 상태로 복원할 수 있습니다. 상태를 관리하는 책임을 Memento
객체로 분리함으로써, 객체의 복잡성을 줄일 수 있습니다.
// Memento 클래스: 상태 저장
public class Memento
{
public string Title { get; }
public string Author { get; }
public Memento(string title, string author)
{
Title = title;
Author = author;
}
}
// Originator 클래스: 상태를 캡처하고 복원
public class Book
{
private string _title;
private string _author;
public Book(string title, string author)
{
_title = title;
_author = author;
}
public void ModifyTitle(string newTitle)
{
_title = newTitle;
}
public void ModifyAuthor(string newAuthor)
{
_author = newAuthor;
}
public Memento SaveState()
{
return new Memento(_title, _author);
}
public void RestoreState(Memento memento)
{
_title = memento.Title;
_author = memento.Author;
}
public void DisplayState()
{
Console.WriteLine($"Book: {_title} by {_author}");
}
}
// Caretaker 클래스: Memento 객체 관리
public class Caretaker
{
private Stack<Memento> _history = new Stack<Memento>();
public void SaveState(Memento memento)
{
_history.Push(memento);
}
public Memento RestoreState()
{
return _history.Count > 0 ? _history.Pop() : null;
}
}
// 클라이언트 코드
public class Program
{
public static void Main(string[] args)
{
var book = new Book("Design Patterns", "Erich Gamma");
var caretaker = new Caretaker();
// 상태 저장
caretaker.SaveState(book.SaveState());
// 상태 변경
book.ModifyTitle("Refactoring");
book.DisplayState(); // Output: Book: Refactoring by Erich Gamma
// 상태 복원
book.RestoreState(caretaker.RestoreState());
book.DisplayState(); // Output: Book: Design Patterns by Erich Gamma
}
}
단일 책임 원칙이 준수
객체의 상태를 저장하고 복원하는 기능이 객체의 핵심 로직과 분리되었으므로, 단일 책임 원칙이 준수됩니다.
객체 상태 관리 편의성 증가
객체의 상태 변경 내역을 손쉽게 관리할 수 있으며, 여러 상태를 캡처하여 필요할 때마다 복원할 수 있습니다.
Memento 패턴 구성 요소
Memento
객체의 상태를 캡처하여 저장하는 역할을 합니다.
Book
객체의 상태(제목, 저자)를 저장합니다.
Originator
객체의 상태를 저장하고 복원하는 기능을 제공합니다.
예제에서는 Book
클래스가 해당 역할을 합니다.
Caretaker
Memento 객체를 관리하고, 저장된 상태를 복원하는 역할을 합니다.
Caretaker
는 상태를 저장하거나 복원할 때 Memento
객체만을 다루며, 객체의 내부 구조를 알 필요가 없습니다.
객체지향 원칙과의 관계
캡슐화
Memento 패턴은 객체의 내부 상태를 보호하는 캡슐화 원칙을 강력하게 지원합니다. Memento 객체는 객체의 상태를 캡처하지만, 외부에서는 그 내부 상태에 직접 접근할 수 없습니다. Memento는 Originator의 상태를 캡처하고 복원하는 데 필요한 데이터를 저장하면서도, Memento의 내부 구조는 Caretaker에게 노출되지 않습니다.
- Caretaker는 Memento의 내부 상태에 접근하지 않고, Memento를 단순히 보관하고 필요할 때 복원만 요청합니다.
- Memento는 객체의 상태를 보호하면서, 캡슐화된 형태로 상태를 관리합니다. 이렇게 하여 내부 상태가 외부로 드러나지 않으므로, 객체의 무결성을 유지할 수 있습니다.
단일 책임 원칙
Memento 패턴은 단일 책임 원칙을 잘 준수합니다. 객체의 상태 저장과 복원 기능을 Memento 객체로 분리함으로써, 원래 객체Originator는 자신의 본래 역할인 비즈니스 로직을 처리하는 데 집중할 수 있습니다.
- 원본 객체Originator는 상태를 생성하고 조작하는 역할을 하고, 상태를 저장하거나 복원하는 것은 Memento로 분리됩니다.
- Caretaker는 Memento를 관리하고 요청이 있을 때 복원하는 책임을 맡습니다.
이렇게 각 객체가 자신의 역할에만 집중하도록 설계되어 유지보수가 용이합니다. 예를 들어,
Book
클래스는 도서의 상태를 변경하는 역할만 담당하고, 상태를 저장하거나 복원하는 로직은 Memento와 Caretaker가 처리합니다.
개방_폐쇄 원칙
새로운 상태 저장 및 복원 방식이 추가되더라도, 기존 클래스들은 수정되지 않고 확장 가능합니다. 예를 들어, Memento에 더 많은 속성을 저장하거나 복원하는 기능을 추가할 수 있지만, 기존의 Book, Caretaker 클래스는 변경할 필요가 없습니다.
- 새로운 기능이 추가되더라도 Memento 객체가 확장되며, 상태 복원 로직만 추가될 뿐 기존 객체의 동작은 그대로 유지됩니다.
- 클라이언트 코드와 Originator는 Memento의 내부 상태에 의존하지 않으므로, 시스템 전체가 쉽게 확장될 수 있습니다.
의존 역전 원칙
Memento 패턴은 의존 역전 원칙을 충족합니다. 클라이언트와 Caretaker는 객체의 상태를 저장하고 복원하는 데 있어서 Memento 객체의 구체적인 구현에 의존하지 않습니다. Originator는 Memento 객체에 의존하긴 하지만, Caretaker는 Memento의 내부 상태에 접근하지 않고 Memento 인터페이스에 의존합니다. 이를 통해, Memento가 변경되더라도 시스템의 다른 부분이 영향을 받지 않도록 설계할 수 있습니다.
맺음말
Memento 패턴은 객체의 상태를 안전하게 캡처하고, 필요할 때 복원할 수 있는 강력한 패턴입니다. 이를 통해 객체의 상태 변경 내역을 쉽게 관리할 수 있으며, 단일 책임 원칙과 캡슐화를 유지할 수 있습니다.
심화학습
메모리 사용량 관리
Memento 패턴을 사용하여 객체의 상태를 저장할 때, Memento 객체가 메모리를 많이 차지할 수 있습니다. 특히, 객체의 상태를 자주 캡쳐 하거나, 복잡한 상태를 저장하는 경우, Memento 객체의 메모리 사용량이 급격히 증가할 수 있습니다.
상태 스냅샷 최적화
필요한 부분만 선택적으로 저장하여 Memento 객체의 크기를 줄일 수 있습니다. 객체의 전체 상태를 저장하는 대신, 변경된 부분만 캡처하는 방식을 도입하면 메모리 사용을 줄일 수 있습니다.
public class OptimizedMemento
{
public Dictionary<string, object> ChangedStates { get; private set; }
public OptimizedMemento(Dictionary<string, object> changes)
{
ChangedStates = changes;
}
}
압축 기법
복잡한 상태 데이터를 압축하여 저장하거나, 파일 시스템에 데이터를 임시로 기록하는 방법을 사용할 수 있습니다. 압축 및 해제 비용이 있지만, 메모리 사용량을 줄이는 데 유리할 수 있습니다.
Memento 객체의 개수 제한
Memento 객체를 관리할 때, 일정한 개수 이상의 Memento가 생성되면 가장 오래된 Memento를 제거하는 방식으로 메모리 사용을 제한할 수 있습니다. 이를 통해 필요 이상의 메모리 사용을 방지할 수 있습니다.
가비지 컬렉션 및 주기적 정리
메모리 관리 및 성능 향상을 위해, 일정 기간마다 사용되지 않는 Memento 객체를 정리하는 로직을 추가할 수 있습니다. 이를 통해 메모리 낭비를 방지할 수 있으며, 상태가 지나치게 많이 쌓이는 것을 방지할 수 있습니다.
영속적 저장소 활용
메모리에서 직접 관리하기 어려울 정도로 많은 Memento 객체가 필요한 경우, 파일이나 데이터베이스와 같은 외부 저장소에 상태를 기록하고, 필요할 때만 불러오는 방식으로 관리할 수 있습니다. 이를 통해 메모리 사용량을 줄일 수 있습니다.
객체의 관리 복잡성
객체의 상태가 자주 변경되는 시스템에서는 Memento 객체가 많아질 수 있으며, 이를 효과적으로 관리하는 로직이 복잡해질 수 있습니다. 특히, 과거 상태로 되돌아가거나 여러 상태를 비교해야 하는 경우, 관리 로직이 복잡해질 수 있습니다.
상태 관리 전략 도입
Memento 객체를 효율적으로 관리하기 위해, 상태 관리 전략State Management Strategy을 도입할 수 있습니다. 예를 들어, Undo/Redo 기능을 효율적으로 구현하기 위해, 상태를 스택 구조로 저장하여 필요한 만큼의 Memento만 유지하는 방식입니다.
public class Caretaker
{
private Stack<Memento> _undoStack = new Stack<Memento>();
private Stack<Memento> _redoStack = new Stack<Memento>();
public void SaveState(Memento memento)
{
_undoStack.Push(memento);
_redoStack.Clear(); // 새로운 상태 저장 시 Redo 스택 초기화
}
public Memento Undo() => _undoStack.Count > 0 ? _undoStack.Pop() : null;
public Memento Redo() => _redoStack.Count > 0 ? _redoStack.Pop() : null;
}
비동기 환경에서의 Memento
Memento 패턴을 비동기 환경에서 적용할 경우, 상태가 변경되는 시점과 복원되는 시점이 일치하지 않을 수 있습니다. 이를 해결하기 위해 Memento 객체를 비동기적으로 처리하는 방법을 고려할 수 있습니다.
public async Task SaveStateAsync(Book book, Caretaker caretaker)
{
await Task.Run(() => caretaker.SaveState(book.SaveState()));
}
위 예제에서는 Memento 객체를 비동기적으로 저장하는 방식으로 상태 저장을 처리할 수 있습니다.
불변 객체와 Memento
상태를 캡처할 때, 객체가 불변immutable 객체인 경우에도 Memento 패턴을 적용할 수 있습니다. 불변 객체의 경우, 새로운 객체를 생성하여 상태를 변경하는 방식으로 관리할 수 있으며, Memento 객체를 통해 이전 상태를 저장해둘 수 있습니다.
public class ImmutableBook
{
public string Title { get; }
public string Author { get; }
public ImmutableBook(string title, string author)
{
Title = title;
Author = author;
}
public ImmutableBook ModifyTitle(string newTitle)
{
return new ImmutableBook(newTitle, this.Author);
}
}
Composite Memento
여러 개의 객체가 서로 관련된 상태를 가지고 있는 경우, 각 객체의 Memento를 관리하는 Composite Memento
를 사용할 수 있습니다.
public class CompositeMemento
{
public Memento BookState { get; }
public Memento AuthorState { get; }
public CompositeMemento(Memento bookState, Memento authorState)
{
BookState = bookState;
AuthorState = authorState;
}
}
복합적인 상태를 저장하고 복원할 때, 이러한 방식으로 여러 상태를 관리할 수 있습니다.
public void RestoreCompositeState(CompositeMemento memento)
{
this.RestoreState(memento.BookState);
this.RestoreState(memento.AuthorState);
}
Memento 와 Command
커맨드 패턴은 사용자의 명령을 객체로 캡슐화하는 패턴으로, 실행 취소Undo와 재실행Redo 기능을 쉽게 구현할 수 있습니다. 메멘토 패턴은 이러한 Undo/Redo 기능을 지원하기 위해 상태를 저장하는 역할을 합니다. 메멘토 패턴을 사용하여 명령 객체가 상태를 변경하기 전에 원본 객체의 상태를 저장해두고, 필요할 때 저장된 상태로 복구할 수 있습니다. 커맨드 패턴과 함께 메멘토 패턴을 사용하면 시스템의 상태가 변경되기 전에 언제든지 상태를 저장하고 복구할 수 있는 기능을 쉽게 추가할 수 있습니다.
// 커맨드 패턴과 메멘토 패턴의 결합 예시
// Command 인터페이스
public interface ICommand
{
void Execute();
void Undo();
}
// 메멘토를 활용한 상태 관리
public class Originator
{
private string _state;
public void SetState(string state)
{
_state = state;
Console.WriteLine($"State set to: {_state}");
}
public Memento SaveState() => new Memento(_state);
public void RestoreState(Memento memento)
{
_state = memento.GetState();
Console.WriteLine($"State restored to: {_state}");
}
}
// Memento 클래스
public class Memento
{
private string _state;
public Memento(string state) => _state = state;
public string GetState() => _state;
}
// 커맨드 클래스
public class SetStateCommand : ICommand
{
private Originator _originator;
private Memento _memento;
private string _newState;
public SetStateCommand(Originator originator, string newState)
{
_originator = originator;
_newState = newState;
}
public void Execute()
{
_memento = _originator.SaveState(); // 현재 상태를 저장
_originator.SetState(_newState); // 새로운 상태 설정
}
public void Undo()
{
_originator.RestoreState(_memento); // 저장된 상태로 복구
}
}
// 클라이언트 코드
public class Program
{
public static void Main(string[] args)
{
Originator originator = new Originator();
ICommand command = new SetStateCommand(originator, "New State");
command.Execute(); // 상태 변경
command.Undo(); // Undo로 상태 복구
}
}
Memento 와 Observer
메멘토 패턴은 객체의 상태를 저장하는 데 초점을 맞추고 있지만, 옵저버 패턴을 결합하면 객체의 상태가 변경될 때마다 자동으로 여러 옵저버에게 알림을 보내고, 상태가 변할 때 그 상태를 메멘토로 저장하는 기능을 추가할 수 있습니다. 도서 관리 시스템에서 책의 대여 상태가 변경될 때마다 옵저버가 이를 감지하고 메멘토 패턴을 이용해 그 상태를 저장해두는 방식으로 확장할 수 있습니다.
// 옵저버 인터페이스
public interface IObserver
{
void Update(string state);
}
// 옵저버 구현 클래스
public class StateLogger : IObserver
{
public void Update(string state)
{
Console.WriteLine($"State changed to: {state}, saving to log.");
}
}
// 메멘토와 옵저버 패턴의 결합
public class ObservableOriginator
{
private List<IObserver> _observers = new List<IObserver>();
private string _state;
public void AddObserver(IObserver observer)
{
_observers.Add(observer);
}
public void SetState(string state)
{
_state = state;
NotifyObservers();
}
private void NotifyObservers()
{
foreach (var observer in _observers)
{
observer.Update(_state);
}
}
public Memento SaveState() => new Memento(_state);
public void RestoreState(Memento memento) => _state = memento.GetState();
}
// 클라이언트 코드
public class Program
{
public static void Main(string[] args)
{
var originator = new ObservableOriginator();
var logger = new StateLogger();
originator.AddObserver(logger);
originator.SetState("Available");
originator.SetState("Borrowed");
}
}
Memento 와 Factory
메멘토 객체의 생성을 관리하기 위해 팩토리 패턴을 활용할 수 있습니다. 상태를 저장할 때마다 직접 메멘토 객체를 생성하는 대신, 팩토리를 통해 객체를 생성하면 객체 생성의 책임을 분리할 수 있습니다. 도서 관리 시스템에서 상태 저장 시 팩토리 패턴을 통해 메멘토 객체를 생성하여 관리할 수 있습니다. 이렇게 하면 상태 저장 로직이 깔끔해지고, 메멘토 객체의 생성 로직을 쉽게 변경할 수 있습니다.
// Memento 팩토리
public class MementoFactory
{
public Memento CreateMemento(string state)
{
return new Memento(state);
}
}
// Memento 사용 코드
public class OriginatorWithFactory
{
private string _state;
public void SetState(string state)
{
_state = state;
}
public Memento SaveState(MementoFactory factory)
{
return factory.CreateMemento(_state);
}
}
// 클라이언트 코드
public class Program
{
public static void Main(string[] args)
{
MementoFactory factory = new MementoFactory();
OriginatorWithFactory originator = new OriginatorWithFactory();
originator.SetState("Available");
Memento memento = originator.SaveState(factory);
// 이후 복원 등에서 Memento 사용 가능
}
}
Memento 와 Strategy
메멘토 패턴은 객체의 상태를 저장하는 방식으로 작동합니다. 이때, 상태 저장의 전략을 전략Strategy 패턴을 이용해 동적으로 바꿀 수 있습니다. 예를 들어, 메모리 성능을 고려해 상태를 모두 저장하는 대신 부분적으로 저장하는 전략을 선택할 수 있습니다.
// 상태 저장 전략 인터페이스
public interface IStateSaveStrategy
{
Memento SaveState(string state);
}
// 전체 상태 저장 전략
public class FullStateSaveStrategy : IStateSaveStrategy
{
public Memento SaveState(string state) => new Memento(state);
}
// 일부 상태 저장 전략
public class PartialStateSaveStrategy : IStateSaveStrategy
{
public Memento SaveState(string state)
{
// 상태의 일부만 저장
string partialState = state.Substring(0, 5); // 예: 첫 5글자만 저장
return new Memento(partialState);
}
}
// 메멘토 사용 코드
public class OriginatorWithStrategy
{
private string _state;
private IStateSaveStrategy _saveStrategy;
public OriginatorWithStrategy(IStateSaveStrategy saveStrategy)
{
_saveStrategy = saveStrategy;
}
public void SetState(string state) => _state = state;
public Memento SaveState() => _saveStrategy.SaveState(_state);
}
// 클라이언트 코드
public class Program
{
public static void Main(string[] args)
{
IStateSaveStrategy strategy = new FullStateSaveStrategy();
OriginatorWithStrategy originator = new OriginatorWithStrategy(strategy);
originator.SetState("State to be fully saved");
Memento memento = originator.SaveState();
// 상태 저장 전략을 변경
strategy = new PartialStateSaveStrategy();
originator = new OriginatorWithStrategy(strategy);
originator.SetState("Another state to be partially saved");
memento = originator.SaveState();
}
}
Memento 와 Singleton
메멘토 패턴에서 상태를 저장하고 관리하는 Caretaker를 싱글톤 패턴으로 구현하면, 애플리케이션 내에서 단일 상태 관리 객체를 유지할 수 있습니다. 도서 관리 시스템에서 여러 곳에서 상태를 저장할 때 케어테이커 객체를 싱글톤으로 구현하여 상태 관리의 일관성을 유지할 수 있습니다.
// 싱글톤 케어테이커
public class CaretakerSingleton
{
private static CaretakerSingleton _instance;
private Stack<Memento> _mementos = new Stack<Memento>();
private CaretakerSingleton() { }
public static CaretakerSingleton GetInstance()
{
if (_instance == null)
{
_instance = new CaretakerSingleton();
}
return _instance;
}
public void SaveState(Memento memento) => _mementos.Push(memento);
public Memento RestoreState() => _mementos.Pop();
}