Memento[Design Pattern]

SnowCat·2023년 3월 22일
0

Design Pattern

목록 보기
18/23
post-thumbnail

의도

  • 메멘토 -> 객체의 세부 사항을 공개하지 않으면서 해당 객체의 이전 상태를 저장하고 복원할 수 있게 해주는 행동 디자인 패턴

문제점

  • 텍스트 편집기 앱을 만들고 있다 가정해보자. 편집기에는 텍스트 이외에도 텍스트 서식, 인라인 이미지 삽입 등의 기능을 지원한다.
  • 텍스트 편집기에서 실행 취소 작업을 구현한다 가정해보자. 이 때 상태 저장은 스토리지에 저장을 하기로 결정했다.
  • 그런데 텍스트 편집기 내부의 클래스 요소들의 일부가 private일 수 있다. 이들을 공개로 바꾸면 클래스가 매우 취약해지고, private를 유지하면 상태를 저장할 수 없는 문제에 직면하게 된다.

해결책

  • 상태 스냅샷 생성을 정보를 가지고 있는 객체에 위임
  • 생성한 객체만이 접근할 수 있는 메멘토에 객체 상태의 복사본을 저장하고, 다른 객체들은 메멘토 인터페이스만을 사용해 통신하도록 함
  • 메멘토에는 스냅샷에서 필요한 메타데이터만을 가져오면 되며, 메멘토들을 보관하기 위한 객체로 케어테이커를 사용
  • 원래 객체들은 케어테이커의 메멘토를 가져와 상태 변경이 가능하지만, 케어테이커 자체는 메멘토의 정보를 변경할 수 없음

구조

/**
 * 실제 작업을 수행하는 객체로 자신의 스냅샷을 생성하고 스냅샷에서 상태를 복원할 수 있음
 */
class Originator {
    /**
     * 코드의 편의성을 위해 상태를 단일 string에 저장한다 가정
     */
    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}`);
    }
}

/**
 * 메멘토 인터페이스로 Originator의 스냅샷을 저장하는 역할을 함
 */
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;
    }
}

/**
 * 케어테이커에서는 Originator의 상태를 캡쳐, 복원해야아는 조건을 알고 있어야 함
 * 메멘토들의 스택을 저장하며 오리지네이터의 기록을 추적 가능
 */
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.');
// Originator: My initial state is: Super-duper-super-puper-super.
const caretaker = new Caretaker(originator);

caretaker.backup(); //Caretaker: Saving Originator's state...

originator.doSomething();
/*
Originator: I'm doing something important.
Originator: and my state has changed to: qXqxgTcLSCeLYdcgElOghOFhPGfMxo
*/

caretaker.backup(); //Caretaker: Saving Originator's state...
originator.doSomething();
/*
Originator: I'm doing something important.
Originator: and my state has changed to: iaVCJVryJwWwbipieensfodeMSWvUY
*/

caretaker.backup(); //Caretaker: Saving Originator's state...
originator.doSomething();
/*
Originator: I'm doing something important.
Originator: and my state has changed to: oSUxsOCiZEnohBMQEjwnPWJLGnwGmy
*/

console.log('');
caretaker.showHistory();
/*
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...)
*/

console.log('\nClient: Now, let\'s rollback!\n');
caretaker.undo();
/*
Caretaker: Restoring state to: 2019-02-17 15:14:05 / (iaVCJVryJ...)
Originator: My state has changed to: iaVCJVryJwWwbipieensfodeMSWvUY
*/

console.log('\nClient: Once more!\n');
caretaker.undo();
/*
Caretaker: Restoring state to: 2019-02-17 15:14:05 / (qXqxgTcLS...)
Originator: My state has changed to: qXqxgTcLSCeLYdcgElOghOFhPGfMxo
*/

적용

  • 객체의 이전 상태를 복원할 수 있도록 객체 상태의 스냅샷을 생성하려는 경우에 사용
    메멘토는 객체의 상태를 별도로 저장할 수 있도록 함
  • 객체의 필드, getter, setter에 접근하는게 캡슐화를 위반할 때 사용
    메멘토 패턴을 사용하면 객체가 스스로의 상태 스냅샷을 생성하고 이를 다른 객체가 읽지 못하게 할 수 있음

구현 방법

  1. 어떤 클래스가 Originator의 역할을 할 것인지 결정
  2. 메멘토 클래스를 만들고, Originator 필드를 미러링하는 필드의 집합 설정
    이 때 메멘토 클래스는 변경할 수 없도록 해야함
  3. Originator의 내부에 메멘ㅌ를 중첩하고 메멘토들을 생성, 복원하는 메서드들을 추가함
  4. 메멘토를 Originator로부터 언제 어떻게 요청하는지를 아는 케어테이커를 생성하고, 메멘토 클래스가 둘을 중재할 수 있도록 해줌

장단점

  • 캡슐화를 위반하지 않고 객체 상태의 스냅샷을 생성할 수 있음
  • 케어테이커가 Originaotr 상태의 기록을 유지하도록 해 Originator의 코드를 단순화할 수 있음
  • 클라이언트가 메멘토를 너무 많이 생성하면 앱이 많은 ram을 소모할 수 있음
  • 케어테이커는 메멘토를 파괴할 수 있도록 Originator의 수명주기를 추적해야 함
  • 자바스크립트에서는 메멘토 내의 상태가 그대로 유지된다 보장할 수 없음

출처:
https://refactoring.guru/ko/design-patterns/memento

profile
냐아아아아아아아아앙

0개의 댓글