객체의 구현 세부 사항을 공개하지 않으면서 객체의 이전 상태를 저장하고 복원할 수 있게 해주는 행동 패턴
텍스트 에디터 앱 가정
텍스트에 실행된 작업을 실행 취소하는 기능을 구현해야 하는 상황
→ 작업 실행 전에 앱이 모든 객체의 상태를 저장해 저장소에 저장하도록 하고, 실행 취소를 하면 기록에서 가장 최근 스냅샷을 가져와 모든 객체의 상태를 복원하기로 했음
스냅샷 → 객체의 모든 필드를 살펴보며 저장소에 값들을 저장해야 함 → 대부분 실제 객체들은 중요한 데이터들을 private 필드들에 감춰 놓음
객체들의 모든 상태가 public이라 가정 → 스냅샷 문제는 해결되지만, 필드 추가/제거나 에디터 클래스 리팩토링 등을 하면 이에 영향을 받는 객체들의 상태를 복사하는 클래스들도 바뀌어야 함
스냅샷 - 최소한 실제 텍스트, 커서 좌표, 스크롤 위치 등 값을 컨테이너에 저장해야 함 → 에디터의 상태를 미러링하는 많은 필드들 보유
다른 객체들이 스냅샷 데이터를 읽거나 쓸 수 있게 하려면 필드들이 public이여야 함 → 모든 에디터 상태가 노출됨
다른 클래스들은 스냅샷 클래스의 모든 사소한 변경들에 의존하게 됨
클래스들의 모든 세부 사항을 노출시키거나, 상태 접근을 제한해 스냅샷을 만들 수 없게 되거나, 둘 중 하나의 상황
위 문제들은 잘못된 캡슐화로 인해 발생
메멘토 패턴 - 스냅샷 생성 작업을 실제 상태의 소유자인 오리지네이터에게 위임 → 다른 객체들이 에디터의 상태를 외부에서 복사하려고 하는 대신, 에디터 클래스 스스로가 스냅샷을 만들 수 있음
메멘토는 주로 케어테이커라는 객체 내부에 저장됨
위 텍스트 에디터 예시
메멘토 패턴의 고전적인 구현 - 중첩 클래스
1. 오리지네이터 - 자신의 상태 스냅샷을 생산하고, 필요에 의해 스냅샷의 상태로 복원할 수 있는 클래스
2. 메멘토 - 오리지네이터 상태의 스냅샷 역할을 하는 값 객체
- 메멘토는 주로 불변으로 만들어 생성자를 통해 한 번만 데이터 전달
3. 케어테이커 - 오리지네이터의 상태를 ‘언제’ 그리고 ‘왜’ 저장할지, 언제 복원할 지 알고 있음
- 메멘토 스택을 통해 오리지네이터의 기록 추적, 오리지네이터가 과거로 돌아가야 할 때
케어테이커가 스택의 최상단에 위치한 메멘토를 가져와 오리지네이터의 복원 메서드로 전달
- 이 구현에서 메멘토 클래스는 오리지네이터 내부에 중첩되어 있음
→ 오리지네이터가 메멘토의 private 필드와 메서드에 엑세스 가능
- 케어테이커는 메멘토의 필드와 메서드에 대해 매우 제한된 접근 권한을 가짐
→ 메멘토 저장은 가능하지만 상태 변경은 불가능
중첩 클래스를 지원하지 않는 언어에 알맞는 구현
1. 중첩 클래스가 없으므로 케어테이커가 명시적으로 선언된 중간 인터페이스를 통해서만
메멘토와 협업할 수 있게 해서 메멘토의 필드에 대한 접근 제한
- 중간 인터페이스는 메멘토의 메타데이터와 관련된 메서드만 선언
2. 오리지네이터는 메멘토 객체에 선언된 필드와 메서드에 접근하며 메멘토와 직접 협업
- 메멘토의 모든 멤버를 public 선언해야 한다는 단점 존재
다른 클래스들이 메멘토를 통해 오리지네이터의 상태에 접근하는 일말의 가능성조차 없애려 할 때
1. 이 구현에선 다양한 유형의 오리지네이터와 메멘토를 가질 수 있음,
각 오리지네이터는 대응되는 메멘토 클래스와 협업함, 오리지네이터와 메멘토 둘 다 누구에게도 상태를 노출하지 않음
2. 케어테이커는 명시적으로 메멘토에 저장된 상태를 변경할 수 없게 됨
- 복원 메서드가 메멘토 클래스에 정의되어 있기 때문에 케어테이커 클래스는 오리지네이터에 의존하지 않게 됨
3. 각 메멘토는 자신을 생성한 오리지네이터와 연결됨,
오리지네이터는 자기 자신을 상태 값들과 함께 메멘토의 생성자에 전달
- 메멘토와 오리지네이터의 가까운 관계로 인해, 오리지네이터가 적절한 setter를 정의했다면
메멘토는 오리지네이터의 상태를 복원할 수 있음
- 메멘토 패턴 - 객체의 private 필드를 포함한 완전한 상태의 복사본을 만들어 객체와 별도로 저장할 수 있게 해줌
- 많은 사람들이 실행 취소로 이 패턴을 기억하지만, 트랜잭션 처리에도 이 패턴이 필요 (에러 발생 시 롤백)
- 메멘토는 객체가 스스로의 상태 스냅샷을 생성하게 하고 다른 객체들이 스냅샷을 읽을 수 없게 해
원본 객체 상태 데이터를 안전하게 만듦
1. 오리지네이터 역할을 할 클래스 결정,
프로그램이 해당 타입의 중심 객체 하나를 사용할 지 여러 개의 작은 객체들을 사용할 지 알아야 함
2. 메멘토 클래스 생성, 오리지네이터 클래스 내부에 선언된 필드들을 미러링하는 필드 집합 선언
3. 메멘토 클래스를 불변으로 만듦, 메멘토는 데이터를 생성자를 통해 단 한 번만 받아야 함, setter가 없어야 함
4. 언어에서 중첩 클래스를 지원한다면 메멘토를 오리지네이터 내부에 중첩시킴,
아니라면 메멘토 클래스에서 빈 인터페이스를 추출해 다른 객체들이 해당 인터페이스를 통해 메멘토를 참조하게 함
- 인터페이스에 메타데이터 작업들을 추가해도 되지만 오리지네이터의 상태를 노출시키면 안 됨
5. 오리지네이터 클래스에 메멘토 생성 메서드 추가,
오리지네이터는 자신의 상태를 메멘토 생성자의 인수를 통해 메멘토에게 전달해야 함
- 만약 이전 단계에서 인터페이스를 추출했다면 해당 메서드의 반환 타입은 인터페이스 타입이어야 함
- 메멘토 생성 메서드는 메멘토 클래스와 직접 협업해야 함
6. 오리지네이터의 상태를 복원하는 메서드를 오리지네이터 클래스에 추가, 메멘토 객체를 인수로 받아야 함
- 이전 단계에서 인터페이스를 추출했다면 인터페이스를 파라미터의 타입으로 만들어야 함
→ 오리지네이터는 객체에 대한 완전한 접근 권한이 필요하므로 들어오는 객체를 메멘토 클래스로 타입 변환해야 함
7. 케어테이커는 언제 오리지네이터로부터 새 메멘토를 요청할 지, 어떻게 저장할 지,
언제 오리지네이터를 특정 메멘토로 복원할 지 알아야 함
8. 케어테이커와 오리지네이터 사이의 연결은 메멘토 클래스 내부로 이동 가능
- 각 메멘토는 자신을 생성한 오리지네이터와 연결되어 있어야 함
- 복원 메서드 또한 메멘토 클래스로 이동
- 메멘토 클래스가 오리지네이터 내부에 중첩되어 있거나 오리지네이터 클래스가
자신의 상태를 override할 충분한 setter를 제공하는 경우에만 가능
- 캡슐화를 위반하지 않고 객체의 상태 스냅샷 생성 가능
- 케어테이커가 오리지네이터의 상태 history를 유지하게 해 오리지네이터의 코드를 단순화할 수 있음
- 클라이언트들이 메멘토를 너무 자주 만들면 많은 RAM 소모
- 오래된 메멘토를 파괴하려면 케어테이커가 오리지네이터의 생명 주기를 추적해야 함
- PHP, Python, JavaScript같은 동적 언어는 대부분 메멘토 내부의 상태가 그대로 유지된다는 보장이 없음
- undo 구현 시 커맨드 패턴과 메멘토 패턴을 함께 사용할 수 있음
- 커맨드 - 대상 객체에 작업을 수행하는 역할
- 메멘토 - 커맨드가 실행되기 직전에 객체의 상태를 저장하는 역할
- 메멘토를 이터레이터와 함께 사용해 현재 순회 상태를 저장하고 필요한 경우 롤백 가능
- 때로 프로토타입 패턴이 메멘토 패턴의 간단한 대안이 될 수 있음
→ 상태를 기록에 저장하려는 객체가 간단하고 외부 리소스에 대한 링크가 없거나 링크를 재설정하기 쉬운 경우에 가능
/**
* The Originator holds some important state that may change over time. It also
* defines a method for saving the state inside a memento and another method for
* restoring the state from it.
*/
class Originator {
/**
* For the sake of simplicity, the originator's state is stored inside a
* single variable.
*/
private state: string;
constructor(state: string) {
this.state = state;
console.log(`Originator: My initial state is: ${state}`);
}
/**
* The Originator's business logic may affect its internal state. Therefore,
* the client should backup the state before launching methods of the
* business logic via the save() method.
*/
public doSomething(): void {
console.log('Originator: I\'m doing something important.');
this.state = this.generateRandomString(30);
console.log(`Originator: and my state has changed to: ${this.state}`);
}
private generateRandomString(length: number = 10): string {
const charSet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
return Array
.apply(null, { length })
.map(() => charSet.charAt(Math.floor(Math.random() * charSet.length)))
.join('');
}
/**
* Saves the current state inside a memento.
*/
public save(): Memento {
return new ConcreteMemento(this.state);
}
/**
* Restores the Originator's state from a memento object.
*/
public restore(memento: Memento): void {
this.state = memento.getState();
console.log(`Originator: My state has changed to: ${this.state}`);
}
}
/**
* The Memento interface provides a way to retrieve the memento's metadata, such
* as creation date or name. However, it doesn't expose the Originator's state.
*/
interface Memento {
getState(): string;
getName(): string;
getDate(): string;
}
/**
* The Concrete Memento contains the infrastructure for storing the Originator's
* state.
*/
class ConcreteMemento implements Memento {
private state: string;
private date: string;
constructor(state: string) {
this.state = state;
this.date = new Date().toISOString().slice(0, 19).replace('T', ' ');
}
/**
* The Originator uses this method when restoring its state.
*/
public getState(): string {
return this.state;
}
/**
* The rest of the methods are used by the Caretaker to display metadata.
*/
public getName(): string {
return `${this.date} / (${this.state.substr(0, 9)}...)`;
}
public getDate(): string {
return this.date;
}
}
/**
* The Caretaker doesn't depend on the Concrete Memento class. Therefore, it
* doesn't have access to the originator's state, stored inside the memento. It
* works with all mementos via the base Memento interface.
*/
class Caretaker {
private mementos: Memento[] = [];
private originator: Originator;
constructor(originator: Originator) {
this.originator = originator;
}
public backup(): void {
console.log('\nCaretaker: Saving Originator\'s state...');
this.mementos.push(this.originator.save());
}
public undo(): void {
if (!this.mementos.length) {
return;
}
const memento = this.mementos.pop();
console.log(`Caretaker: Restoring state to: ${memento.getName()}`);
this.originator.restore(memento);
}
public showHistory(): void {
console.log('Caretaker: Here\'s the list of mementos:');
for (const memento of this.mementos) {
console.log(memento.getName());
}
}
}
/**
* Client code.
*/
const originator = new Originator('Super-duper-super-puper-super.');
const caretaker = new Caretaker(originator);
caretaker.backup();
originator.doSomething();
caretaker.backup();
originator.doSomething();
caretaker.backup();
originator.doSomething();
console.log('');
caretaker.showHistory();
console.log('\nClient: Now, let\'s rollback!\n');
caretaker.undo();
console.log('\nClient: Once more!\n');
caretaker.undo();
// Output.txt
Originator: My initial state is: Super-duper-super-puper-super.
Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: qXqxgTcLSCeLYdcgElOghOFhPGfMxo
Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: iaVCJVryJwWwbipieensfodeMSWvUY
Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: oSUxsOCiZEnohBMQEjwnPWJLGnwGmy
Caretaker: Here's the list of mementos:
2019-02-17 15:14:05 / (Super-dup...)
2019-02-17 15:14:05 / (qXqxgTcLS...)
2019-02-17 15:14:05 / (iaVCJVryJ...)
Client: Now, let's rollback!
Caretaker: Restoring state to: 2019-02-17 15:14:05 / (iaVCJVryJ...)
Originator: My state has changed to: iaVCJVryJwWwbipieensfodeMSWvUY
Client: Once more!
Caretaker: Restoring state to: 2019-02-17 15:14:05 / (qXqxgTcLS...)
Originator: My state has changed to: qXqxgTcLSCeLYdcgElOghOFhPGfMxo
참고 자료: Refactoring.guru