의도
- 메멘토 -> 객체의 세부 사항을 공개하지 않으면서 해당 객체의 이전 상태를 저장하고 복원할 수 있게 해주는 행동 디자인 패턴
문제점
- 텍스트 편집기 앱을 만들고 있다 가정해보자. 편집기에는 텍스트 이외에도 텍스트 서식, 인라인 이미지 삽입 등의 기능을 지원한다.
- 텍스트 편집기에서 실행 취소 작업을 구현한다 가정해보자. 이 때 상태 저장은 스토리지에 저장을 하기로 결정했다.
- 그런데 텍스트 편집기 내부의 클래스 요소들의 일부가 private일 수 있다. 이들을 공개로 바꾸면 클래스가 매우 취약해지고, private를 유지하면 상태를 저장할 수 없는 문제에 직면하게 된다.
해결책
- 상태 스냅샷 생성을 정보를 가지고 있는 객체에 위임
- 생성한 객체만이 접근할 수 있는 메멘토에 객체 상태의 복사본을 저장하고, 다른 객체들은 메멘토 인터페이스만을 사용해 통신하도록 함
- 메멘토에는 스냅샷에서 필요한 메타데이터만을 가져오면 되며, 메멘토들을 보관하기 위한 객체로 케어테이커를 사용
- 원래 객체들은 케어테이커의 메멘토를 가져와 상태 변경이 가능하지만, 케어테이커 자체는 메멘토의 정보를 변경할 수 없음
구조
class Originator {
private state: string;
constructor(state: string) {
this.state = state;
console.log(`Originator: My initial state is: ${state}`);
}
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('');
}
public save(): Memento {
return new ConcreteMemento(this.state);
}
public restore(memento: Memento): void {
this.state = memento.getState();
console.log(`Originator: My state has changed to: ${this.state}`);
}
}
interface Memento {
getState(): string;
getName(): string;
getDate(): string;
}
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', ' ');
}
public getState(): string {
return this.state;
}
public getName(): string {
return `${this.date} / (${this.state.substr(0, 9)}...)`;
}
public getDate(): string {
return this.date;
}
}
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());
}
}
}
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();
적용
- 객체의 이전 상태를 복원할 수 있도록 객체 상태의 스냅샷을 생성하려는 경우에 사용
메멘토는 객체의 상태를 별도로 저장할 수 있도록 함
- 객체의 필드, getter, setter에 접근하는게 캡슐화를 위반할 때 사용
메멘토 패턴을 사용하면 객체가 스스로의 상태 스냅샷을 생성하고 이를 다른 객체가 읽지 못하게 할 수 있음
구현 방법
- 어떤 클래스가 Originator의 역할을 할 것인지 결정
- 메멘토 클래스를 만들고, Originator 필드를 미러링하는 필드의 집합 설정
이 때 메멘토 클래스는 변경할 수 없도록 해야함
- Originator의 내부에 메멘ㅌ를 중첩하고 메멘토들을 생성, 복원하는 메서드들을 추가함
- 메멘토를 Originator로부터 언제 어떻게 요청하는지를 아는 케어테이커를 생성하고, 메멘토 클래스가 둘을 중재할 수 있도록 해줌
장단점
- 캡슐화를 위반하지 않고 객체 상태의 스냅샷을 생성할 수 있음
- 케어테이커가 Originaotr 상태의 기록을 유지하도록 해 Originator의 코드를 단순화할 수 있음
- 클라이언트가 메멘토를 너무 많이 생성하면 앱이 많은 ram을 소모할 수 있음
- 케어테이커는 메멘토를 파괴할 수 있도록 Originator의 수명주기를 추적해야 함
- 자바스크립트에서는 메멘토 내의 상태가 그대로 유지된다 보장할 수 없음
출처:
https://refactoring.guru/ko/design-patterns/memento