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

문제
텍스트 편집기 앱을 만들고 있다고 상상해보세요. 당신의 편집기는 간단한 텍스트 편집 외에도 텍스트의 서식 지정, 인라인 이미지들의 삽입 등을 할 수 있습니다.
어느 날 당신은 사용자들이 텍스트에 수행된 모든 작업을 실행 취소할 수 있도록 하기로 했습니다. 이 실행 취소 기능은 수년에 걸쳐 매우 보편화되었기 때문에 오늘날의 사용자들은 모든 앱에 이 기능이 있을 것이라고 가정합니다. 이 기능을 구현하기 위해 직접 접근 방식을 적용하기로 했습니다. 앱은 모든 작업을 수행하기 전에 모든 객체의 상태를 기록해 어떤 스토리지에 저장합니다. 나중에 사용자가 작업을 실행 취소하기로 하면 앱은 기록에서 가장 최신 스냅샷을 가져와 모든 객체의 상태를 복원하는 데 사용합니다.

앱은 작업을 실행하기 전에 객체들의 상태의 스냅샷을 저장하며, 이 스냅샷은 나중에 객체들을 이전 상태로 복원하는 데 사용할 수 있습니다.
상태 스냅샷들에 대해 생각해 봅시다. 상태 스냅샷은 정확히 어떻게 생성될까요? 아마도 객체의 모든 필드를 살펴본 후 해당 값들을 스토리지에 복사해야 할 것입니다. 그러나 이는 객체의 내용에 대한 액세스 제한이 상당히 완화되어 있는 경우에만 작동할 것입니다. 불행히도, 대부분의 실제 객체들은 모든 중요한 데이터를 비공개 필드에 숨깁니다.
이 문제는 일단 무시하고, 객체들이 히피족처럼 열린 관계들을 선호해 그들의 상태를 공개했다고 가정해 봅시다. 이렇게 가정하면 일단 위의 문제는 해결되어 원하는 대로 객체들의 상태에 대한 스냅샷들을 생성할 수 있습니다. 하지만 여전히 몇 가지 심각한 문제들이 남아 있습니다. 앞으로 당신이 일부 필드를 추가 또는 제거하거나, 편집기 클래스들을 리팩토링하기로 결정할지도 모르기 때문입니다. 말은 쉬워 보이지만, 그렇게 하려면 영향받은 객체들의 상태를 복사하는 역할을 맡은 클래스들을 변경해야 합니다.

객체의 비공개 상태는 어떻게 복사할까요?
그뿐만이 아닙니다. 편집기 상태의 실제 '스냅샷'들에 어떤 데이터가 포함되어 있는지 살펴봅시다. 이 안에는 최소한 실제 텍스트, 커서 좌표, 현재 스크롤 위치 등이 포함되어 있을 겁니다. 스냅샷을 만들려면 이러한 값들을 수집한 후 일종의 컨테이너에 넣어야 합니다.
아마도 당신은 이러한 컨테이너 객체들을 기록에 해당하는 어떤 리스트에 많이 저장하게 될 겁니다. 따라서 이 컨테이너들은 결국 한 클래스의 객체들이 될 것입니다. 이 클래스에는 메서드는 거의 없을 테지만, 편집기의 상태를 미러링하는 필드는 많이 있을 겁니다. 다른 객체들이 스냅샷에서 데이터를 읽고 스냅샷에 데이터를 쓸 수 있도록 하려면, 아마도 해당 스냅샷의 필드를 공개해야 할 것입니다. 그러면 편집기의 모든 (비공개 포함) 상태들이 노출될 것이고, 이제 다른 클래스들은 스냅샷 클래스에 발생하는 모든 자그마한 변경에도 영향을 받게 될 것입니다. 편집기의 모든 상태가 노출되지 않았다면 이러한 변경들은 외부 클래스에는 영향을 미치지 않은 채 비공개 필드와 메서드 안에서 변경이 발생하는 것으로 끝났을 겁니다.
이제 교착 상태에 빠진 것 같습니다. 클래스 내부의 세부 정보를 모두 공개하면 클래스가 너무 취약해집니다. 하지만 클래스의 상태에 접근하지 못하게 하면 스냅샷을 생성할 수 없게 됩니다. 그러면 '실행 취소'는 대체 어떻게 구현해야 할까요?
해결책
우리가 방금 경험한 모든 문제는 캡슐화의 실패로 인해 발생합니다. 일부 객체들은 원래 해야 할 일보다 더 많은 일들을 수행하려고 합니다. 예를 들어 이러한 객체들은 어떤 작업을 수행하는 데 필요한 데이터를 수집하기 위해 다른 객체들이 실제 작업을 수행하도록 놔두는 대신 그들의 비공개 공간을 침범합니다.
메멘토는 상태 스냅샷들의 생성을 해당 상태의 실제 소유자인 originator(오리지네이터) 객체에 위임합니다. 그러면 다른 객체들이 '외부'에서 편집기의 상태를 복사하려 시도하는 대신, 자신의 상태에 대해 완전한 접근 권한을 갖는 편집기 클래스가 자체적으로 스냅샷을 생성할 수 있습니다.
이 패턴은 메멘토라는 특수 객체에 객체 상태의 복사본을 저장하라고 제안합니다. 메멘토의 내용에는 메멘토를 생성한 객체를 제외한 다른 어떤 객체도 접근할 수 없습니다. 다른 객체들은 메멘토들과 제한된 인터페이스를 사용해 통신해야 합니다. 이러한 인터페이스는 스냅샷의 메타데이터(생성 시간, 수행한 작업의 이름, 등)를 가져올 수 있도록 할 수 있지만, 스냅샷에 포함된 원래 객체의 상태는 가져오지 못합니다.

오리지네이터는 메멘토에 대한 전체 접근 권한이 있지만 케어테이커(caretaker)는 메타데이터에만 접근할 수 있습니다.
이러한 제한 정책을 사용하면 일반적으로 케어테이커라고 하는 다른 객체들 안에 메멘토들을 저장할 수 있습니다. 케어테이커는 제한된 인터페이스를 통해서만 메멘토와 작업하기 때문에 메멘토 내부에 저장된 상태를 변경할 수 없습니다. 동시에 오리지네이터는 메멘토 내부의 모든 필드에 접근할 수 있으므로 언제든지 자신의 이전 상태를 복원할 수 있습니다.
위의 텍스트 편집기 예시의 경우, 별도의 기록 클래스를 만들어 케어테이커의 역할을 하도록 할 수 있습니다. 케어테이커 내부의 메멘토들의 스택은 편집기가 작업을 실행하려고 할 때마다 계속 늘어날 것입니다. 또 당신은 앱의 UI 내에서 이 스택을 렌더링하여 이전에 수행한 작업들의 기록을 사용자에게 표시할 수도 있습니다.
사용자가 실행 취소를 작동시키면 기록은 스택에서 가장 최근의 메멘토를 가져온 후 편집기에 다시 전달하여 롤백을 요청합니다. 편집기는 메멘토에 대한 완전한 접근 권한이 있으므로 메멘토에서 가져온 값들로 자신의 상태를 변경합니다.
의사코드
이 예시에서는 메멘토를 커맨드 패턴과 함께 사용하여 복잡한 텍스트 편집기의 상태의 스냅샷들을 저장하고 필요할 때 스냅샷들로부터 이전 상태를 복원할 수 있도록 합니다.

텍스트 편집기 상태에 대한 스냅샷 저장.
커맨드 객체들은 케어테이커 역할을 합니다. 이 객체들은 커맨드들과 관련된 작업들을 실행하기 전에 편집기의 메멘토를 가져옵니다. 사용자가 가장 최근 커맨드를 실행 취소하려고 하면 편집기는 해당 커맨드에 저장된 메멘토를 사용하여 자신을 이전 상태로 되돌릴 수 있습니다.
메멘토 클래스는 공개된 필드들, 게터(getter)들 또는 세터(setter)들을 선언하지 않습니다. 따라서 어떤 객체도 자신의 내용을 변경할 수 없습니다. 메멘토들은 자신을 만든 편집기 객체에 연결됩니다. 이것은 메멘토가 데이터를 연결된 편집기 객체의 세터들을 통해 전달하여 해당 편집기의 상태를 복원할 수 있도록 합니다. 메멘토들은 특정 편집자 객체들에 연결되어 있으므로 당신은 당신의 앱이 중앙 집중식 실행 취소 스택을 사용하여 여러 독립 편집기 창을 지원하도록 할 수 있습니다.
/**
* 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.
*/
class Memento {
public:
virtual ~Memento() {}
virtual std::string GetName() const = 0;
virtual std::string date() const = 0;
virtual std::string state() const = 0;
};
/**
* The Concrete Memento contains the infrastructure for storing the Originator's
* state.
*/
class ConcreteMemento : public Memento {
private:
std::string state_;
std::string date_;
public:
ConcreteMemento(std::string state) : state_(state) {
this->state_ = state;
std::time_t now = std::time(0);
this->date_ = std::ctime(&now);
}
/**
* The Originator uses this method when restoring its state.
*/
std::string state() const override {
return this->state_;
}
/**
* The rest of the methods are used by the Caretaker to display metadata.
*/
std::string GetName() const override {
return this->date_ + " / (" + this->state_.substr(0, 9) + "...)";
}
std::string date() const override {
return this->date_;
}
};
/**
* 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 {
/**
* @var string For the sake of simplicity, the originator's state is stored
* inside a single variable.
*/
private:
std::string state_;
std::string GenerateRandomString(int length = 10) {
const char alphanum[] =
"0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
int stringLength = sizeof(alphanum) - 1;
std::string random_string;
for (int i = 0; i < length; i++) {
random_string += alphanum[std::rand() % stringLength];
}
return random_string;
}
public:
Originator(std::string state) : state_(state) {
std::cout << "Originator: My initial state is: " << this->state_ << "\n";
}
/**
* 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.
*/
void DoSomething() {
std::cout << "Originator: I'm doing something important.\n";
this->state_ = this->GenerateRandomString(30);
std::cout << "Originator: and my state has changed to: " << this->state_ << "\n";
}
/**
* Saves the current state inside a memento.
*/
Memento *Save() {
return new ConcreteMemento(this->state_);
}
/**
* Restores the Originator's state from a memento object.
*/
void Restore(Memento *memento) {
this->state_ = memento->state();
std::cout << "Originator: My state has changed to: " << this->state_ << "\n";
}
};
/**
* 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 {
/**
* @var Memento[]
*/
private:
std::vector<Memento *> mementos_;
/**
* @var Originator
*/
Originator *originator_;
public:
Caretaker(Originator* originator) : originator_(originator) {
}
~Caretaker() {
for (auto m : mementos_) delete m;
}
void Backup() {
std::cout << "\nCaretaker: Saving Originator's state...\n";
this->mementos_.push_back(this->originator_->Save());
}
void Undo() {
if (!this->mementos_.size()) {
return;
}
Memento *memento = this->mementos_.back();
this->mementos_.pop_back();
std::cout << "Caretaker: Restoring state to: " << memento->GetName() << "\n";
try {
this->originator_->Restore(memento);
} catch (...) {
this->Undo();
}
}
void ShowHistory() const {
std::cout << "Caretaker: Here's the list of mementos:\n";
for (Memento *memento : this->mementos_) {
std::cout << memento->GetName() << "\n";
}
}
};
/**
* Client code.
*/
void ClientCode() {
Originator *originator = new Originator("Super-duper-super-puper-super.");
Caretaker *caretaker = new Caretaker(originator);
caretaker->Backup();
originator->DoSomething();
caretaker->Backup();
originator->DoSomething();
caretaker->Backup();
originator->DoSomething();
std::cout << "\n";
caretaker->ShowHistory();
std::cout << "\nClient: Now, let's rollback!\n\n";
caretaker->Undo();
std::cout << "\nClient: Once more!\n\n";
caretaker->Undo();
delete originator;
delete caretaker;
}
int main() {
std::srand(static_cast<unsigned int>(std::time(NULL)));
ClientCode();
return 0;
}
'디자인 패턴' 카테고리의 다른 글
의존성 주입 (0) | 2025.04.08 |
---|---|
Observer 패턴 (3) | 2024.09.05 |
Mediator 패턴 (0) | 2024.09.05 |
iterator 패턴 (1) | 2024.09.05 |
Command 패턴 (0) | 2024.09.04 |