디자인 패턴/GoF

[디자인 패턴] 행동 패턴 (9) 메멘토 Memento

로파이 2021. 3. 29. 00:38

메멘토 (Memento) 패턴

 

1. 의도

 

어떤 객체의 내부 상태를 기록하는 상태 객체를 관리하고 필요할 때 지난 상태로 돌아갈 수 있도록 한다.

 

2. 활용

 

게임 플레이어의 체크포인트를 기록하거나 그래픽/텍스트 편집기에서 실행했던 명령을 되돌리는 방법, 오류에서 복구하는 방법 등 활용성이 높은 기능으로 과거의 상태나 명령들을 저장해두었다 필요할 때 거꾸로 되돌리기 위해 필요한 정보를 제공한다.

 

그러한 기능이 필요한 객체의 모든 정보를 노출시키지 않고 상태에 쓰이는 정보만 주기적으로 저장하며 필요할 때 요청하도록한다.

 

그래픽 편집기의 그리기 명령어와 되돌리기 수행 절차

1. 편집기는 사용자가 마우스로 다양한 객체를 그릴 때마다 메멘토에 요청하여 현재 상태를 저장하도록 한다.

2. 메멘토는 필요한 상태 정보를 저장한 인스턴스를 실행 순서대로 저장하도록 한다.

3. 사용자가 그리기 전으로 돌아가는 요청을 했을 때, 가장 마지막 실행을 한 명령을 순서대로 편집기에 돌려준다.

4. 편집기는 그린 객체를 취소할 수도 있고 지워진 객체를 다시 그릴 수도 있다.

 

3. 참여자

Memento 패턴

  • Memento(메멘토): 원조 객체의 내부 상태를 저장한다. 관리 책임을 갖는 Caretaker 클래스는 Memento에 대해 제한 범위의 인터페이스만을 볼 수 있다. Originator 클래스는 Memento의 모든 인터페이스를 볼 수 있다.
  • Originator(그래픽 편집기): 원조 본 객체로 메멘토를 생성하여 객체를 저장하고 복원한다.
  • CareTaker(취소 실행 메커니즘): 메멘토의 보관을 책임지는 보관자. 메멘토의 내용을 검사하거나 내용을 건드리지 않는다.

4. 협력 방법

  • 보관자(CareTaker) 객체는 원조본 객체(Originator)에게 메멘토를 생성해달라고 요청한다.
  • 되돌리고자 하는 상태가 있을 때 보관자는 메멘토를 이용하여 객체 상태를 되돌린다.

5. 결과

 

  1) 캡슐화된 경계를 유지한다.

  • 원조 본만 메멘토 객체를 다룰 수 있기 때문에 메멘토가 외부에 노출되지 않는다.

  2) Originator 클래스를 단순화할 수 있다.

  • 필요한 상태만 남겨두어 Originator 클래스를 단순화한다.

  3) 메멘토의 사용으로 비용이 들어간다.

  • 자주 메멘토를 사용하거나 반환한다면 메멘토가 가져오는 오버헤드가 크다.

  4) 메멘토를 관리하는 데 필요한 비용이 있다.

  • 보관자 객체가 메멘토를 생성 및 삭제해야 하며 얼마나 유지해야 할지도 결정해야 한다.

6. 구현

 

1. 클래스 접근 제한자 기능을 제공하는 언어에서 자신의 인터페이스를 노출할 클래스를 설정할 수 있다.

C++: friend class

메멘토 객체는 원조본 객체를 friend로 선언하여 원조본 객체의 상태를 설정하거나 가져오는 인터페이스를 원조본만 사용할 수 있도록한다.

class State;
class Memento;

class Originator {
public:
	Memento* CreateMemento();
	void SetMemento(const Memento*);
	//...
private:
	State* _state; // 내부 자료구조
};

class Memento {
public:
	// 좁은 범위의 인터페이스들
	virtual ~Memento();
private:
	// 원조본 객체만 접근할 수 있는 private 멤버들
	friend class Originator;
	Memento();
	void SetState(State*);
	State* GetState();
	//...
private:
	State* _state;
};

2. 점증적 상태 변경을 저장한다.

변경된 명령어나 상태만을 저장하고 취소될 때는 명령을 거꾸로 실행하거나 상태를 다시 설정하는 것이 가능해야한다.

 

예제 코드

- ConstaintSolver

그래픽 편집기에서 두 객체에 대한 제약으로 한 객체를 움직이면 다른 객체를 자동적으로 그려주는 기능이 있다하자.

그래픽 편집기에서 두 개의 그래픽와 두 객체의 위치에 대한 제약 (상대적으로 반드시 얼만큼 위치해야하는 지와 같은)이 있고 움직일 때마다 이 상태를 기록하고 되돌리는 방법을 메멘토를 통해 구현해본다.

 

MoveCommand.h

한 객체를 움직이는 명령어 대한 클래스로 움직이는 명령 Execute()와 되돌리는 명령 Unexecute()에 대한 함수가 존재한다. 내부에 되돌릴 때 현재 상태를 의미하는 ConstaintSolverMemento와 명령이 어떤 그래픽을 움직였고 얼마나 움직였는 지를 기록한다.

class Graphic;
class MoveCommand
{
public:
	// Graphic 객체를 delta만큼 움직이는 명령어
	MoveCommand(Graphic* target, const Point& delta)
		:
		_target(target), _delta(delta)
	{

	}
	void Execute();
	void Unexecute();
private:
	// 연결된 그래픽 객체들의 상태
	class ConstraintSolverMemento* _state;
	// 어느 그래픽 객체를 어느 만큼 움직였는지
	Point _delta;
	Graphic* _target;
};

 

ConstraintSolver.h

Originator 클래스로 그래픽 편집기를 의미하며 두 그래픽 요소간 제약을 추가하거나 제거할 수 있다. MoveCommand가 되돌리는 연산을 구현하기 위해 메멘토를 요청했을 때, 생성한 ConstaintSolverMemento 인스턴스를 반환하거나 설정할 수 있다.

// 단일체 패턴
class ConstraintSolver
{
public:
	static ConstraintSolver* Instance();

	void Solve();
	// 그래픽 객체간 연결을 추가
	void AddConstraint(Graphic* start, Graphic* end);
	// 그래픽 객체간 연결을 삭제
	void RemoveConstraint(Graphic* start, Graphic* end);

	// 메멘토 상태를 만들어 반환한다.
	ConstraintSolverMemento* CreateMemento();
	void SetMemento(ConstraintSolverMemento*);
private:
	// 연결을 처리하기 위한 필요한 상태나 연산을 여기에 정의한다.
};

ConstaintSolverMemento.h

ConstraintSolver를 표현하는 미공개 정보를 저장한다.

class ConstraintSolverMemento
{
public:
	virtual ~ConstraintSolverMemento();
private:
	friend class ConstraintSolver;
	ConstraintSolverMemento();
	// ConstraintSolver의 미공개용 상태
};

 

MoveCommand는 메멘토 객체를 요청하여 얻은 상태와 내부에서 가지고 있던 해당 그래픽 인스턴스와 얼만큼 움직였는지에 대한 정보를 가지고 Execute()와 되돌리는 연산 Unexecute()를 구현한다.

void MoveCommand::Execute()
{
	ConstraintSolver* solver = ConstraintSolver::Instance();
	_state = solver->CreateMemento();
	_target->Move(_delta);
	solver->Solve();
}

void MoveCommand::Unexecute()
{
	ConstraintSolver* solver = ConstraintSolver::Instance();
	_state = solver->CreateMemento();
	_target->Move(-_delta);
	solver->Solve();
}