메멘토는 구현의 상세 내역을 표시하지 않고 객체의 이전 상태를 저장하고 복원할 수 있는 동작 설계 패턴이다.
→ 객체의 상태에 대한 스냅샷을 만듦
스냅샷 내부에 보관된 데이터뿐만 아니라 작업하는 객체의 내부 구조를 손상 시키지 않음
텍스트 편집기 앱을 만든다고 가정해보자. 편집기는 텍스트 편집 외에 형식 지정, 인라인 이미지 삽입과 같은 다양한 작업을 수행할 수 있다.
어느 시점에 사용자가 텍스트 편집에 대한 되돌리기 기능을 사용한다고 해보자. 이런 기능을 구현하려면 작업을 수행하기 전, 앱의 모든 객체 상태를 기록하고 일부 스토리지에 저장해놓아야 한다. 그래야만 나중에 사용자가 작업을 되돌릴때, 스냅샷을 가져와 모든 객체 상태를 복원하는 데 사용될 수 있다.
작업을 실행하기 전에 앱은 객체 상태의 스냅샷을 저장하며, 나중에 객체를 이전 상태로 복원하는 데 사용할 수 있다.
그 상태 스냅샷들에 대해 생각해보자. 정확히 어떻게 만들 수 있을까? 객체의 모든 필드를 검토하고 해당 값을 스토리지에 복사해야하는데 이는 객체에 대한 접근 제한이 가능할때만 가능하다. 그러나 대부분의 실제 객체들은 자신의 내부를 private
하게 감추어두기 때문에 힘들다.
지금은 이 문제를 객체가 privated이 아닌 public이라고 생각하여 해결해보자. 그러나 이렇게 한다고 하더라도 객체 상태에 대한 스냅샷을 생성할 순 있지만 몇 가지 문제가 있다. 나중에 일부 편집기 클래스를 리팩터링하거나 일부 필드를 추가하거나 제거할 수 있는데, 이 경우 영향을 받는 객체의 상태를 복사하는 클래스도 변경해야 한다.
객체의 프라이빗 상태를 어떻게 복사할 수 있을까?
또한 이렇게 만들게 되면 문제가 더 있는데 텍스트 편집기와 관련된 스냅샷을 생각해보면 실제 텍스트, 커서좌표, 현재 스크롤 위치등이 포함되어 있을 것이다. 이러한 값을 수집하여 일종의 컨테이너에 넣어놓아야 하는데 대부분의 컨테이너는 객체를 많이 저장하기 위해 클래스의 형태를 띌것이다. 이 경우, 다른 객체가 스냅샷에서 데이터를 쓰고 읽을 수 있게 하려면 해당 필드를 공개해야 한다. 이렇게 되면 다른 클래스는 스냅샷 클래스의 모든 사소한 변경 사항에 종속된다.
클래스의 모든 내부 세부 정보를 노출하여 너무 취약하게 만들거나 상태에 대한 액세스를 제한하여 스냅샷을 생성할 수 없도록 하는 등 Undo(되돌리기)
라는 기능을 수행하는데 다른 방법이 없을까?
위에서 본 문제들이 발생한 이유는 캡슐화가 깨져서 발생한 것이다. 어떤 객체들은 일부 작업을 수행하는 데 필요한 데이터를 수집하기 위해 다른 객체의 개인 공간을 침범하고 있었다.
메멘토 패턴은 상태 스냅샷을 생성하는 것을 해당 상태의 실제 소유자인 원본 객체로 위임한다. 따라서 밖에서 편집기의 상태를 복사하려고 하는 다른 객체 대신, 편집기 클래스는 자신의 상태에 대한 완전한 권한을 가지므로 스냅샷 자체를 만들 수 있다.
이 패턴은 객체의 상태 복사본을 메멘토라고 불리는 특별한 객체에 저장하여 메멘토에 대한 내용은 메멘토만 접근이 가능하고, 다른 객체는 스냅샷의 메타데이터(생성 시간, 수행된 작업의 이름)를 가져올 수 있지만 스냅샷에 포함된 원래 객체의 상태는 가져올 수 없는 제한된 인터페이스를 사용하여 메멘토와 통신해야한다.
원본자는 메멘토에 대한 완전한 접근 권한을 가지는 반면, 관리인(caretaker)은 메타데이에만 접근할 수 있다.
이러한 제한 정책을 사용하면 일반적으로 관리인(caretaker)이라고 하는 다른 객체 안에 메멘토를 저장할 수 있다. 관리인은 제한된 인터페이스를 통해서만 메멘토와 함께 작동하여 메멘토안의 저장된 상태를 수정할 수 없고, 원본자는 메멘토 내의 모든 필드에 액세스하여 원하는 대로 이전 상태를 복원할 수 있다.
텍스트 편집기 예제에서는 관리자로서 작동한 별도의 히스토리 클래스를 만들어 메멘토 스택을 잉요하여 사용자가 실행 취소를 트리거하면 기록이 스택에서 가장 최근의 메멘토를 가져와 다시 편집기로 전달하여 롤백을 요청한다. 편집기는 메멘토에 대한 전체 액세스 권한을 가지므로 메멘토에서 가져온 값에 따라 자체 상태를 변경한다.
중첩된 클래스 기반 구현
패턴의 고전적인 구현은 중첩 클래스에 대한 지원에 의존하며 C++, C#, 자바와 같은 프로그래밍 언어에서 사용할 수 있다.
Originator 클래스는 자체 상태의 스냅샷을 생성할 수 있을뿐만 아니라 필요할 때 스냅샷에서 상태를 복원할 수도 있다.
메멘토는 원본자의 상태에 대한 스냅샷 역할을 하는 값 객체이다. 메멘토를 불변으로 만들어 생성자를 통해 단 한 번만 데이터를 전달하는 것이 일반적이다.
케어테이커는 언제와 왜, 원본자의 상태를 캡처해야 하는지 뿐만 아니라 상태가 복원되어야 하는 시점도 알고 있다.
케이테이커는 메멘토 스택을 저장함으로써 원본자의 기록을 추적할 수 있다.
이 구현에서 메멘토 클래스는 Originator 안에서 중첩된다. 이렇게 하면 원본자가 개인 문서로 선언된 경우에도 메멘토의 필드와 메서드에 액세스할 수 있다. 반면, 관리인은 메멘토의 필드 및 메서드에 대한 권한이 매우 제한적이어서 메멘토를 스택에 저장하지만 상태를 변조하지는 않는다.
중간 인터페이스를 기반으로 구현
중첩 클래스를 지원하지 않는 프로그래밍 언어(PHP)에 적합한 구현체
더욱 엄격한 캡슐화를 통한 구현 → 아래 코드 구조가 이렇게 되어 있음
다른 클래스가 메멘토를 통해 오리지네이터 상태에 접근할 가능성을 조금이라도 남겨두고 싶지 않을 때 유용한 또 다른 구현이 있다.
객체 이전의 상태를 복원할 수 있도록 객체 상태의 스냅샷을 생성하려면 메멘토 패턴을 사용하라.
객체의 field/getter/setter에 대한 직접 액세스가 캡슐화를 위반할 경우 패턴을 사용하라.
캡슐화를 위반하지 않고 객체 상태의 스냅샷을 생성할 수 있다.
관리인이 원본자 상태 기록을 유지 관리하도록 하여 원본자의 코드를 단순화할 수 있다.
클라이언트가 메멘토를 너무 자주 생성하면 앱에서 RAM을 많이 소모할 수 있다.
관리인은 오래된 메멘토를 파기할 수 있도록 작성자의 수명 주기를 추적해야한다.
PHP, 파이썬, JS와 같은 대부분의 동적 프로그래밍 언어들은 메멘토 내의 상태를 그대로 유지하는 것을 보장하지 못한다.
복잡도: ★★★
인기: ★☆☆
사용 예: 메멘토의 원리는 타입스크립트에서 직렬화를 사용하여 구현할 수 있다. 객체 상태의 스냅샷을 만드는 유일한 방법은 아니지만 원본 객체의 구조를 다른 객체로부터 보호하면서 상태 백업을 저장할 수 있다.
index.ts
// 원본자는 시간이 지남에 따라 변경될 수 있는 몇가지 중요한 상태를 유지한다.
// 또한 메멘토 내부의 상태를 저장하는 메서드와 그것으로부터 상태를 복원하는 다른 메서드를 정의한다.
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();
console.log(caretaker.showHistory());
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();
결과
메멘토 패턴은 이전 버전의 기록을 저장하고 복원할 수 있는 스냅샷을 활용한 동작 설계 패턴이다.
코드를 설계할 때, 캡슐화를 깨뜨리지 않고 원본자의 상태 기록을 관리인쪽으로 넘겨 코드를 단순화하는데 도움을 준다.
디자인 패턴에 대한 예시를 찾다가 계속해서 눈에 보이는 블로그가 있는데 들어가서 보면 좋을 것 같다.
⬇️