Memento Pattern의 취지와 목적은?
Memento는 구현 세부 사항을 밝히지 않고 객체의 이전 상태를 저장하고 복원 할 수있는 행동 설계 패턴이다.
즉, private한 field들의 상태를 바꾸지 않고(public이나, protected 등..), 객체들에게 접근 용이하도록 만들기 위한 패턴이다.
다음의 예제를 보며 이해하도록 한다.
Problem
text editor 앱을 만들고 있다고 가정하자. 간단한 텍스트 편집 말고도, 텍스트의 format, image 삽입 등을 지원하도록 만들 것이다.
이후에, 유저가 자신이 text 편집기에서 수행한 작업을 다시 되돌릴 수 있도록 만들기로 결정했다. 이것의 구현을 위해서, 어떤 작업을 수행하기 전에 앱은 모든 object들의 상태를 저장하고, 그것을 storage에 저장하기로 했다. 이후에, 유저가 어떤 작업을 되돌리기로 결정했다면 앱은 최신 스냅샷을 가져온 후 이것을 이용해서 모든 object의 상태를 복원하도록 했다.
작업을 실행하기 전에 앱은 객체 상태의 스냅 샷을 저장하고 나중에 이 객체를 이전 상태로 복원하는 데 사용할 수 있다.
이러한 state snapshots는 어떻게 구현해야 할까? 아마도 모든 객체의 field를 살펴본 후, 그 값들을 storage에 저장해야 할 것이다. 그러나 이것은 object에 access 제한이 강력하지 않을 경우에만 작동할 것이다.
하지만 실제 구현에서는 object들은 다른 유저들이 쉽게 내부를 들여다 볼 수 없도록 하여 field의 중요한 data들을 모두 숨겨 버리기 때문에, 위처럼 구현하는 것은 좋지 않다.
하지만 지금은 그 문제를 무시하고, 모든 object의 field state가 public이며, open된 relations라고 생각해보자. 이러한 접근 방식은 즉시 현 문제를 해결 가능하며 object들의 snapshots를 생성할 수 있지만, 여전히 심각한 이슈가 존재한다.
나중에, 이 text editor class를 refactoring하거나, 일부 field를 추가하거나 제거하기로 한다면, 이 때 영향을 받는 object들의 state들을 복사하여 저장하는 클래스들을 변경해야 될 것이다.
여기서, 실제 어떤 text editor에서의 snapshots state를 고려해 본다면, 최소한 실제 텍스트, 커서 좌표, 현재 스크롤의 위치 등이 포함되어 있어야 할 것이다. 하나의 snapshot을 만들기 위해서는 이러한 값들을 모아서 어떠한 컨테이너에 넣어야 한다.
대부분의 경우, 이 object의 컨테이너를 이용해서 많은 text 저장 결과들을 기록해야 한다. 따라서 컨테이너는 한 클래스의 objects가 될 것이다. 이 클래스에는 method는 거의 없고, editor의 상태를 반영하는 많은 field가 존재하게 된다. 그리고 다른 objects가 snapshot에서 데이터를 읽고 쓰도록 하려면 아마 이 class의 field들을 모두 public state로 만들어야 한다.
결국 snapshot을 저장하기 위해서는 field들이 public이 되어야 한다. 다른 클래스는 snapshot 클래스의 아주 작은 변경에 dependency가 생기며, 그렇지 않으면 private field를 만드는 대신 외부 클래스에 영향을 미치지 않을 수 있지만, access가 제한되어 구현이 어렵게 된다.
실행 취소(undo)에 대한 다른 구현 방법이 존재할까?
Solution
모든 문제는 결국 encapsulation이 깨져서 발생하게 되는 것이다. 일부 object들을 이용해서 어떤 작업을 수행할 때, 필요한 데이터를 수집하기 위해서 어떤 다른 object들의 private space를 침범해야하는 경우가 생길 수 있다.
Memento Pattern은 snapshot 작성을 현재 상태의 실제 owner인 originator object에 맡긴다. 따라서 “외부”에서 editor의 상태를 복사하려는 다른 object 대신, editor class 자체가 자기 자신의 상태에 대해 모든 access 권한이 있으므로 snapshot을 만들 수 있게 된다.
즉, 이 패턴은 object state의 사본(copy)을 memento라고 불리는 어떤 특수한 object에 저장하도록 제안하는 것이다. memento의 content는 자기를 생성한 object를 제외한 다른 object에는 access할 수 없다. 다른 object들은 snapshot의 metadata(생성 시간, 수행한 작업의 이름 등..)를 가져올 수 있지만, snapshot에 포함되어 있는 원래 object들의 상태는 가져 올 수 없는 제한된 interface를 사용하여 커뮤니케이션 해야한다.
originator는 memento에 대한 전체 access 권한을 가지지만, caretaker는 오직 metadata에만 access 가능하다.
이런 제한적인 정책을 이용하면 일반적으로 caretakers라고 불리는 objects에 memento를 저장할 수 있게 된다. caretaker는 제한된 interface를 통해서만 memento를 이용할 수 있으므로, memento 내부에 저장된 state를 조작할 수 없게 된다. 동시에 originator는 field에 모든 access를 지니고 있으므로 원하는대로 이전 상태로 field 값을 복원시킬 수 있다.
text editor 예제에서, caretaker 역할을 하는 별도의 history class를 생성할 수 있다. Editor가 작업을 수행하려고 할 때 마다 caretaker 내부에 저장되어 있는 memento들의 stack이 늘어나게 된다. 앱의 UI에서 이 stack을 랜더링해서 이전에 수행했던 작업 기록을 유저에게 보여줄 수도 있게 된다.
사용자가 undo를 trigger하면, history는 stack에서 가장 최근의 memento를 가져 와서 편집기로 다시 전달한 후 roll-back을 요청한다. Editor는 memento에 대한 전체 access 권한을 가지므로 memento에서 가져온 값으로 자체 상태를 변경한다.
Structure
Implementation based on nested classes
일반적인 구현은 중첩 클래스를 지원하는 것으로 한다. 즉, 다음 구현은 중첩된 클래스가 지원 가능한 언어에서만 가능하다 (C++, C#, Java, …).
Originator 클래스는 자체 상태의 snapshot을 생성하고 필요할 때 snapshot에서 상태를 복원 할 수 있다.
Memento는 originator의 state를 저장하는, 즉 snapshot 역할을하는 value object이다. Memento를 immutable하도록 만들고 생성자를 통해 데이터를 한 번만 전달하는 것이 일반적이다.
Caretaker는 originator의 상태를 “언제”, 또 “왜” 캡쳐해야 하는지 알고있을 뿐 아니라 언제 state를 복원해야 하는 지도 알고 있어야 한다.
Caretaker는 여러개의 memento의 stack을 저장함으로써 originator의 history를 추적할 수 있다. originator가 어떤 지점의 과거로 돌아갸아 할 경우, caretaker는 stack에서 최상위 memento를 가져와서 이를 originator의 restoration method에 전달한다.
이 implementation에서, memento class는 originator에 중첩되어야 한다(nested). 이렇게 함으로써 memento의 field와 method가 private로 선언되어 있더라도 originator가 접근 가능하다. 반면에, caretaker는 memento의 field 및 method들에 제한적인 access를 갖고 있어 stack에 memento들을 저장하지만 상태를 변경할 수는 없게 된다.
Implementation based on an intermediate interface
중첩 클래스를 지원하지 않는 프로그래밍 언어에 대한 적절한 대안법에 대해서 설명한다(PHP).
중첩 클래스가 없는 언어의 경우, 어떤 규칙을 설정함으로써 memento의 field의 access를 제한할 수 있다. 그 규칙이란 Caretaker가 오직 명시적으로 선언된(explicitly declared) intermediary interface를 통해서만 memento와 작업할 수 있으며, 이 interface는 오직 memento의 metadata와 관련된 method만 선언한다.
반면에, originator는 memento class에 선언된 field와 method에게 access가 가능하여 memento object로 직접 작업할 수 있게 된다. 이 방법의 단점은 memento의 모든 멤버들을 public으로 선언해야 한다는 것이다.
Implementation with even stricter encapsulation
다른 클래스가 memento를 통해서 originator의 상태에 access할 최소한의 가능성도 남기지 않기 위한 또 다른 구현법이 존재한다.
이 구현은 multiple type의 originator와 memento들을 가질 수 있다. 각 originator는 그에 대응하는 memento class와 동작한다. originator나 memento들은 그 누구에게도 자신의 state를 공개하지 않는다.
Caretaker는 이제 memento들에 저장된 상태를 변경하지 못하도록 명시적으로 제한(explicitly restricted)된다. 또한 복원(restore)방법이 memento class에 정의되어 있기 때문에 caretaker class는 originator와 독립적(independent)인 관계를 갖게 된다.
각 memento는 자신을 생성한 originator와 연결된다. originator는 자신의 state 값과, 자신 모두를 memento의 생성자에 전달한다. 이 class사이의 밀접한 관계 덕분에, memento는 originator에서 적절한 setter method가 정의되어 있다면 originator의 state를 복원할 수 있게 된다.
Pseudocode
이 예제에서는, 복잡한 text editor 상태의 snapshot을 저장하고, 필요할 때 이 snapshot들에게서 이전 상태를 복원하기 위한 Command 패턴으로 memento 패턴을 사용한다.
command object들은 caretaker 역할을 한다. command와 연관된 작업을 실행하기 전에, editor의 memento를 가져온다(fetch). 유저가 가장 최신의 command를 취소(undo)하고자 할 때 editor는 command에 저장된 memento를 사용해서 이전 상태로 되돌릴 수 있게 된다.
memento class는 public field들, 또는 getter나 setter를 선언하지 않는다. 따라서 그 어떤 object라도 그 내용을 변경할 수 없게 된다. memento들은 그들을 생성한 editor object와 연결되어 있다. 이를 통해서, memento는 editor object의 setter를 통해서 데이터를 전달해서 링크된 editor의 상태를 복원할 수 있게 된다. memento는 특정 editor object에 연결되어 있으므로, 앱이 중앙 undo stack으로 독립적인 editor windows를 지원하도록 만들 수 있다.
1 | // The originator holds some important data that may change over |
Applicability
- 이전 상태의 object를 복원할 수 있도록 object의 상태에 대한 snapshot을 생성하려는 경우 이 패턴을 사용하면 좋을 것이다.
- memento 패턴을 사용하면, private field를 포함하여 object 상태의 전체 copy본을 만들고 object와 별개로 저장할 수 있게 된다. “undo” 사용 경우 말고도 트랜잭션(거래 처리 등)을 처리할 때도 필수적이다(즉, 오류가 나타날 시 작업을 roll back 해야하는 경우).
- object의 field/getter/setter에 대한 직접 access를 하는 경우가 encapsulation을 위반하는 경우 이 패턴을 사용하면 좋다.
- Memento는 object 자체의 상태 snapshot 생성을 책임진다. 다른 object는 snapshot을 읽을 수 없으므로 original object의 상태 데이터를 안전하게 보호할 수 있게 된다.
How to Implement
Determine what class will play the role of the originator. It’s important to know whether the program uses one central object of this type or multiple smaller ones.
Create the memento class. One by one, declare a set of fields that mirror the fields declared inside the originator class.
Make the memento class immutable. A memento should accept the data just once, via the constructor. The class should have no setters.
If your programming language supports nested classes, nest the memento inside the originator. If not, extract a blank interface from the memento class and make all other objects use it to refer to the memento. You may add some metadata operations to the interface, but nothing that exposes the originator’s state.
Add a method for producing mementos to the originator class. The originator should pass its state to the memento via one or multiple arguments of the memento’s constructor.
The return type of the method should be of the interface you extracted in the previous step (assuming that you extracted it at all). Under the hood, the memento-producing method should work directly with the memento class.
Add a method for restoring the originator’s state to its class. It should accept a memento object as an argument. If you extracted an interface in the previous step, make it the type of the parameter. In this case, you need to typecast the incoming object to the memento class, since the originator needs full access to that object.
The caretaker, whether it represents a command object, a history, or something entirely different, should know when to request new mementos from the originator, how to store them and when to restore the originator with a particular memento.
The link between caretakers and originators may be moved into the memento class. In this case, each memento must be connected to the originator that had created it. The restoration method would also move to the memento class. However, this would all make sense only if the memento class is nested into originator or the originator class provides sufficient setters for overriding its state.
Pros and Cons
Pros | Cons |
---|---|
캡슐화를 위반하지 않고 객체 상태의 스냅 샷을 생성 할 수 있다. | 클라이언트가 memento를 너무 자주 생성하게 된다면 앱에서 많은 RAM을 소비할 수 있게 된다. |
Caretaker가 originator의 상태의 history를 유지하도록 하여 originator의 코드를 단순화 할 수 있다. | Caretaker는 사용하지 않는 memento를 destroy할 수 있도록 originator의 lifecycle을 추적해야 한다. |
PHP, Python 및 JS와 같은 대부분의 동적 프로그래밍 언어는 memento내의 상태가 그대로 유지되도록 보장할 수 없다. |
Relations with Other Patterns
- Undo 구현 시 Command와 Memento를 함께 사용할 수 있다. 이 경우 Command는 target object에 대해 다양한 작업을 수행하는 반면에, Memento는 command가 실행되기 전에 해당 object의 상태를 저장한다.
- Iterator와 함께 Memento를 사용하여 현재 iteration state를 캡처한 후 필요한 경우 roll back할 수 있다.
- 때때로 프로토타입은 memento의 간단한 대안점이 될 수 있다. 이는 history에서 저장하고자 하는 상태인 object가 상당히 간단하고, 외부 자원(external resources)과 연결된 것이 없거나 또는 link를 재설정(re-establish)하기 쉬운 경우에 작동한다.