본문 바로가기

study/design pattern

[디자인패턴][행위패턴] 메멘토 Memento - C++

728x90

[모던 C++ 디자인 패턴] 책을 바탕으로 공부하는 내용을 정리한 내용이다.


Memento Pattern

커맨드 디자인 패턴에서 시스템의 모든 변경 이력을 기록한다면, 이론적으로 과거의 어떤 지점으로든 상태를 되돌릴 수 있다는 것을 이미 살펴봤다.

여기서 임의의 과거가 아니라 필요할 때 특정 시점으로 되돌릴 수만 있으면 충분한 경우가 있다. 그것이 메멘토 패턴이 필요한 경우다.

메멘토는 특정 시점의 상태를 전용 객체로 저장해 리턴한다. 이 객체는 읽기 전용 속성을 가지며 그 자체적으로는 어떤 동작도 하지 않는다. 이러한 "토큰"의 객체는 필요할 때 시스템에 주입되어 저장된 상태로 되돌린다.


은행 계좌

앞서 매개자 패턴에서 살펴보았던 은행 계좌 예를 이어서 볼 것이다. BankAccount 클래스의 정의에 입금 기능만 있다고 가정해 보자. 이전에는 이 함수가 아무런 동작도 하지 안 했지만 여기서는 메멘토 객체를 반환할 것이다. 반대로, 메멘토 객체를 이용해 계좌를 메멘토에 저장된 상태로 되돌릴 수 있다.

class BankAccount {
public:
    explicit BankAccount(const int balance) : balance(balance) {}
    
    Memento deposit(int amount) {
        balance += amount;
        return {balance};
    }
    
    void restore(const Memento &m) {
        balance = m.balance;
    }
    
private:    
    int balance = 0;
}

Memento 클래스는 아래와 같이 쉽게 구현 가능하다.

class Memento {
public:
    Memento(int ablance) : balance(balance) {}
    
    friend class BankAccount;
    
private:
    int balance;
}

Memento 클래스는 두 특이점이 있다.

  1. 불변 속성을 갖는다.
  2. BankAccount를 friend로 선언한다. balance 필드 변수에 접근하기 위해 필요하다. friend 대신 Memento 클래스를 BankAccount의 내부 중첩 클래스로 만들 수도 있다.

Undo와 Redo

BankAccount에서 생성되는 Memento를 모두 저장한다면 어떻게 될까? 커맨드 디자인 패턴의 구현에서와 마찬가지로 Undo와 Redo 작업이 가능해진다.

새로운 은행 계좌 클래스 BankAccount2를 만들 것이다. 여기서는 생성되는 모든 Memento를 저장한다.

class BankAccount2 {
public:
    explicit BankAccount2(const int balance) : balance(balance) {
        changes.emplace_back(make_shared<Memento>(balance));
        current = 0;
    }
    
    // undo와 redo 지원
    
private:
    int balance =0 ;
    vector<shared_ptr<Memento>> changes;
    int current;
}

위와 같이 한다면, 계좌 생성 초기 잔고 값이 저장되어 생성자에서 리턴할 수 없는 문제가 해결된다. 생성자가 메멘토를 리턴할 수 있는 것은 아니지만, 되돌리기 작업에는 사용할 수 있다. 초기 상태로 되돌리기 위한 reset() 같은 전용 멤버 함수를 만들 수도 있다.

다음은 deposit() 멤버 함수와 메멘토에 맞춰 계좌의 상태를 되돌리는 restore() 멤버 함수의 구현이다.

shared_ptr<Memento> deposit(int amount) {
    balance += amount;
    auto m = make_shared<Memento>(balance);
    changes.push_back(m);
    ++current;
    return m;
}

void restore(const shared_ptr<Memento> &m) {
    if ( m ) {
        balance = m->balance;
        changes.push_back(m);
        current = chagnes.size() - 1;
    }
}

restore()를 보면 앞서 보았던 것과는 크게 다르다.

  1. 인자 shared_ptr의 유효성 검사가 추가되어, 아무 기능하지 않는 경우가 추가된다.
  2. 메멘토를 복구할 때 변경 작업 자체도 변경 리스트에 추가된다. 되돌리기를 다시 되돌릴 수 있다.

이제 undo()와 redo() 멤버 함수를 보자.

shared_ptr<Memento> undo() {
    if ( current > 0 ) {
        --current;
        auto m = changes[current];
        balance = m->balance;
        return m;
    }
    
    // 이전 메멘토로 되돌릴 수 없는 경우 디폴트 생성자를 통해 공백 포인터 객체 반환
    return {};
}

shared_ptr<Memento> redo() {
    if ( current + 1 < changes.size() ) {
        ++current;
        auto m = changes[current];
        balance = m->balance;
        return m;
    }
    
    return {};
}

요약

메멘토 디자인 패턴은 시스템의 상태를 이전으로 되돌리기 위한 토큰을 어떻게 정의하고 관리할 수 있는지 보여준다. 특정 상태로 옮기기 위해 필요한 만큼의 작은 정보를 저장한 토큰이 필요하다.

728x90