- 참조: https://refactoring.guru/design-patterns/memento
- 메멘토(Memento)
메멘토 패턴은 다른이름으로 "토큰" 이라고도 불리우며, 객체를 저장하거나 객체를 이전 상태로 되돌리기 위한 행동패턴이다.
- 문제점
텍스트 에디터 앱을 만든다고 가정해보자. 간단한 텍스트 편집외에 포맷수정 및 이미지를 삽입하는 기능도 있다. 이 텍스트 에디터 앱을 사용하는 유저에게 실행취소 기능을 제공해준다고 할 때 가장 직관적인 구현방법은 다음과 같을 것이다. 모든 행위를 수행하기 전에 앱의 모든 객체의 상태를 기록하고, 장치에 저장한다. 이후에 유저가 해당 행위를 되돌리기 원할 때, 앱은 저장한 이력에서 마지막 스냅샷을 꺼내와서 모든 객체의 상태를 복구한다.
위의 방법에 대해서 잠시 생각해보자. 아마도 객체의 모든 필드들을 조회해서 값을 복사한 후 저장소로 옮겨야 할것이다. 그러나 이런 구현이 가능하려면 객체가 외부에 대해 자신의 접근 제한을 많이 오픈할때만 제대로 동작한다. 하지만 대부분의 객체들은 private 접근자로 중요한 데이터들을 은닉화 하고 있다.
이 문제를 해겨하기 위해 객체들이 public 접근제어자를 사용해서 자신의 정보를 공개한다고 가정해보자. 이렇게 하면 객체의 상태를 저장하는게 가능하지만, 또 다른 문제가 발생한다. 나중에 에디터 클래스들을 다시 손보거나 필드들을 일부 추가 및 삭제하면 영향받는 객체들의 상태를 복사하는 클래스들을 변경해야 한다.
문제는 여기서 끝나지 않는다. 실제 데이터 상태의 스냅샷을 생각해보자. 특정 스냅샷 1개 안에도 데이터가 엄청날것이다. 최소한 text 가 있을것이고, 커서의 좌표, 스크롤의 위치 정보를 갖고 있어야 한다. 스냅샷을 만들기 위해서 이런 정보들을 모아야 하며, 해당 정보들을 컨테이너 같은 곳에 갖고 있어야 한다.
아마도 이력을 표현하기 위해 이런 컨테이너 객체들을 list 형태로 저장해야 할것이다. 결국 컨테이너들은 한 클래스의 객체가 된다. 해당 클래스는 메소드는 거의 없고, 필드의 대부분은 에디터의 상태를 미러링하는 형태가 될것이다. 다른 객체들이 스냅샷을 쓰고 읽기 위해서, 해당 필드를 public 접근제어자로 선언해야한다. private 접근제어자를 사용여부와 상관없이 모든 에디터의 상태를 외부로 노출하게 된다. 다른 클래스들은 스냅샷 클래스의 작은 변화에 의존성을 갖게 된다.
결국 클래스의 모든 세부사항을 노출시키거나, 상태의 접근을 제한해서 스냅샷을 생산하는게 불가능해지는 상황이 된다. "undo" 기능을 구현하기 위한 다른 대안이 있을까?
- 해결책
앞에서 언급한 모든 문제들은 캡슐화가 깨져서 일어난다. 어떤 객체는 의도된것보다 더 많은것을 하려고 한다. 어떤 행동을 수행하는데 필요한 데이터를 수집하기 위해 실제 행동을 수행하는 객체들에 접근하지 않고, 다른 객체들의 사적인 공간을 침투한다.
메멘토 패턴은 스냅샷 상태를 만드는 일을 실제 주인인 originator 객체에 위임한다. 즉 다른 객체들이 외부에서 에디터의 상태를 복사하지 않고, 접근권한을 모두 갖고 있는 데이터 클래스 자신이 스냅샷을 만들 수 있도록 한다.
이 패턴에서는 객체의 상태를 복사해서 "메멘토(memento)" 라고 불리는 특별한 객체에 저장한다. 메멘토의 내부는 해당 내용을 생성한 객체 외에 다른 객체들은 접근할 수 없다. 원본이 아닌 다른 객체들은 제한된 인터페이스로 스냅샷에 포함된 원본 객체의 상태가 아니라 스냅샷의 메타데이터(생성 시간, 수행된 행위명)만 갖고올 수 있다.
이런 제한적인 정책을 구현하기 위해서는 "caretakers" 라고 하는 다른 객체안에 메멘토를 저장해야 한다. Caretakers 는 제한된 인터페이스로만 메멘토와 협업하기 때문에, 메멘토 안에 저장된 상태를 함부로 변경할 수 없다. 동시에 originator는 메멘토내의 모든 필드에 접근할 수 있기 때문에 이전 상태를 복구할 수 있게 된다.
텍스트 에디터 예제에서 caretaker 역할을 하는 이력 클래스를 따로 분리할 수 있다. caretakers 안에 저장된 메멘토의 stack 은 에디터가 행동을 수행할 때 마다 점점 비대해진다. 심지어 app의 UI 내에 이런 stack을 보여주어서 사용자에게 자신이 수행한 이력을 보여줄수도 있다.
사용자가 실행취소를 수행하면 stack으로부터 가장 최근의 메멘토를 가져와서 에디터로 전달한다. 에디터는 메멘토에 모든 접근권한이 있기 때문에, 메멘토의 값을 참조하여 자신의 상태를 변경한다.
- 구조
중첩 클래스를 이용하여 구현된 메멘토의 구조를 살펴보자.
- Originator: Originator 클래스는 원본 객체를 말한다. 필요할 때 스냅샷으로 부터 해당 상태를 복구할뿐만 아니라, 자기 자신 상태의 스냅샷을 생산하기도 한다.
- Memento: Memento 는 Originator 상태의 스냅샷 역할을 하는 값 객체(Value Object) 이다. 일반적으로 Memento는 생성자를 이용해서 데이터를 한번 만드는 방식을 많이 사용하므로 변경불가능한 객체하다.
- Caretaker: Caretaker 는 Memento의 보관을 책임지는 보관자이며, 메멘토의 내용을 검사하거나 그 내용을 건드리지는 않는다. Caretaker 는 Memento 스택을 저장함으로써 Originator 의 히스토리를 추적한다. Originator 가 이전 상태를 알아야 할 때, Caretaker는 스택으로부터 최상위 원소를 가져와서 Originator 의 복구 메소드로 전달한다.
위 다이어그램의 구현방식을 살펴보면 Memento 클래스는 Originator 안에 중첩 클래스로 선언되었다. 중첩 클래스 방식으로 구현하면 Originator 가 Memento 의 필드와 메소드들이 private 접근 제한자이더라도 접근가능하게 된다. 반면 Caretaker 가 Memento 의 필드와 메소드들에 대해 접근하는것은 제한되어있으며, 스택에서 Memento 를 저장하는것은 가능하지만 상태를 함부로 수정하지는 못한다.
중첩 클래스를 이용한 방법이 아닌 중간 인터페이스를 이용한 구현방법도 있다.
중첩클래스가 없을때에는 caretaker 가 명시적으로 선언된 중간 인터페이스를 통해서만 memento 와 교류할 수 있어서 memento 접근에 제한을 둘 수 있다.
반면 originator 는 memento 객체와 직접적으로 교류할 수 있어서 memento 내의 필드나 메소드들에 접근가능하다. 하지만 이 방법을 사용하려면 memento의 모든 멤버를 public 으로 선언해야 한다.
캡슐화를 적용한 구현방법도 있다. 이 방법은 다른 클래스들이 memento 를 통해서 originator 상태에 접근하는것을 원하지 않을 때 사용하면 유용한 방법이다.
위 다이어그램을 보면 알겠지만 originator와 memento를 인터페이스로 선언하고 있기 때문에 여러 구현 타입을 가질 수 있다. 각 originator는 memento 클래스를 참조하고 있으며, 두 클래스는 자신의 상태를 외부로 노출시키지 않는다.
Caretaker 에는 memento 에 저장된 상태를 변경할 수 없도록 제한이 생긴다. 또한 복구 메소드가 memento 클래스로 이관되었기 때문에 caretaker 클래스는 originator 로부터 독립된다.
각 memento 는 originator 가 만드는 형상을 갖고 있다. Originator 는 자신의 상태값들과 함께 자기 자신을 memento 의 생성자 인자로 넘긴다. 두 클래스의 응집도가 올라간덕분에 memento 는 originator 의 상태를 복구할 수 있게 된다.
- 메멘토(Memento)와 명령(Command)
메멘토는 명령 패턴과 상당히 헷갈린다. 명령 패턴은 redo/undo를 지원할 수는 있지만 요청을 단일객체로 만들 수 있다는것이 핵심이다. 반면 메멘토는 cache/store개념으로 객체의 내부 상태를 저장하고 있다는 것이 핵심이다.
메멘토와 명령은 둘중 하나만 사용해야 하는 개념은 아니다. 메멘토 패턴을 이용하여 객체의 내부 상태를 저장하고 있어야 할 때, 되돌리기 기능제공시 명령 패턴을 함께 사용할 수 있기 때문이다.
- 시나리오
이번 예제에서는 복잡한 에디터 상태의 스냅샷을 저장하고 필요시 스냅샷으로 부터 이전 상태를 복구하기 위해 메멘토 패턴과 함께 커맨드 패턴을 같이 사용한다.
위 다이어그램에서 Command 객체는 Caretaker 의 역할을 담당한다. 커맨드와 연관된 행위를 수행하기 전 에디터의 Memento 를 가져온다. 사용자가 가장 최근 명령에 대해 실행취소를 수행할 때, 에디터는 이전 상태로 자신을 되돌리기 위해 저장된 Memento 를 사용한다.
Memento 클래스는 어떤 public 필드나 getter, setter 를 갖고 있지 않다. 그러므로 어떤 객체도 해당 내용을 수정할 수 없다. Memento 는 에디터 객체가 생성한 시점만 바라볼 수 있다. Memento 가 에디터 객체의 setter 메소드를 통해 데이터를 전달하므로써 에디터의 상태를 복구할 수 있게해준다. Memento 들은 특정 에디터 객체들로 연결되어있기 때문에, 중앙화된 실행취소 스택을 이용해서 몇개의 독립된 에디터 윈도우들도 지원하게 만들수도 있다.
- Java 예제
먼저 Originator에 해당하는 Editor 부터 구현해보자. Editor 클래스는 아래와 같이 간단하게 작성하였다. Editor 에서 createSnapshot() 메소드를 이용하여 Snapshot을 생성한다.
public class Editor {
private String state;
public void setState(String state) {
this.state = state;
}
public String getState() {
return state;
}
public Snapshot createSnapshot() {
return new Snapshot(this, state);
}
}
그 다음 Memento 에 해당하는 Snapshot 클래스를 구현해보자. Snapshot 에서는 Originator와 Originator의 field 인 state 를 생성자 인자로 받는다. 이 클래스의 restore 메소드 에서는 editor 가 제공하는 setter 메소드를 이용해서 생성자인자로 받았던 state 를 이용하여 해당 시점의 Editor 상태로 복구하고 있다.
public class Snapshot {
private Editor editor;
private String state;
public Snapshot(Editor editor, String state) {
super();
this.editor = editor;
this.state = state;
}
public void restore() {
editor.setState(state);
}
}
그 다음 이력을 관리하는 Command 클래스이다. 원래는 Command 와 CommandHistory 를 분리해서 만들어야 하지만 간단한 예제이므로 CommandHistory 역할을 하는 Stack을 내장하였다.
public class Command {
private Stack<Snapshot> backup;
public Command() {
super();
backup = new Stack<>();
}
public void makeBackup(Editor editor) {
Snapshot snapshot = editor.createSnapshot();
backup.push(snapshot);
}
public boolean undo() {
if(backup.isEmpty()) {
System.out.println("There is no snapshot!");
return false;
}
else {
Snapshot snapshot = backup.pop();
snapshot.restore();
return true;
}
}
}
Test 를 해보자. editor에 abc, abcde 등을 입력하면서 중간 중간 저장을 하였다. 그리고 이를 되돌리면서 Snapshot의 에디터 상태를 확인해보는 예제이다.
public static void main(String[] args) {
Command command = new Command();
Editor editor = new Editor();
editor.setState("abc");
command.makeBackup(editor);
editor.setState("abcde");
editor.setState("abcdefg");
command.makeBackup(editor);
editor.setState("ab");
command.makeBackup(editor);
editor.setState("");
IntStream.range(0, 5)
.forEach(i -> {
if(command.undo()) {
System.out.println(editor.getState());
}
});
}
====실행결과====
ab
abcdefg
abc
There is no snapshot!
There is no snapshot!
'Concepts > Design Pattern' 카테고리의 다른 글
행동 패턴 - 상태(State) (0) | 2021.06.19 |
---|---|
행동 패턴 - 옵저버(Observer) (0) | 2021.06.17 |
행동 패턴 - 중재자(Mediator) (0) | 2021.06.11 |
행동 패턴 - 반복자(Iterator) (0) | 2021.06.08 |
행동 패턴 - 명령(Command) (0) | 2021.06.06 |
댓글