책정리/GoF 디자인 패턴

22장 객체의 이전 상태 복원 문제(Memento 패턴)

GONII 2019. 2. 6. 03:57
    • 문제 사례 설명
    바둑에서 수를 무르는 경우, 바둑판은 이전 상태로 되돌아갈 있어야 한다.
    • 다양한 접근 방법 및 MEMENTO 패턴
      • 기본적인 방법: 계산을 통한 이전 상태 복원 방식
    바둑판 객체는 현재의 바둑판 상태 정보를 저장, 관리한다고 하였다. 이를 위해 바둑판 객체는 [그림 22-1]에서 보는 것처럼 기본적으로 바둑돌이 놓여질 바둑판 정보와 돌이나 검은 돌의 사석 , 다음 돌의 색깔, 두어진 , 패의 위치, 처음 돌의 색깔 등을 데이터 멤버로 가지고 있다.


    가지 생각해볼 있는 방법은 board_[][] 이차원 배열이 바둑 돌이 놓여진 수순 정보를 가지고 있다는 사실을 이용해서 계산을 통해 무르기를 수행하는 것이다. board_[][] 이차원 배열에서 항목들이 가진 절대 값이 바로 바둑 돌이 놓여진 수순을 가리키므로 이를 이용해서 무르기를 수행하는 것이다. 예를 들어 바로 직전에 두었던 돌을 무르기 하려면 board_[][] 이차원 배열에서 절대 값이 가장 위치를 찾아 곳의 값을 0으로 변경시키면 것이다.
    이처럼 수순 정보를 이용해서 무르기를 돌의 위치를 찾아내고, 위치의 바둑 돌을 없애는 형태로 무르기를 수행하는 방식은 언뜻 모든 경우에 정상적으로 동작할 같지만 가지 문제가 있다.
    board_[][] 이차원 배열은 현재 바둑판에 놓여진 돌들에 한해서만 위치 정보를 가지고 있다. 따라서 이전에 두었던 돌이라 하더라도 상대편이 잡혀 바둑판에서 드러내진 돌에 대한 정보는 가지고 있지 않다. 그러므로 board_[][] 이차원 배열에 저장된 정보만 이용해서 무르기를 수행하는 것은 사석으로 바둑판에서 드러내진 돌들에 대해서는 무르기를 하지 못하는 문제가 발생할 있다.
    다른 문제는 돌이나 검은 돌의 사석 수나 패의 위치 등과 같은 정보를 이전 상태로 되돌리기 어렵다는 것이다. 왜냐하면 사석 수나 패의 위치와 같은 데이터 멤버는 현재 상태 값만을 가지고 있는 것으로 무르기가 수행되었을 이전 상태 값을 계산해낼 있는 방법이 없기 때문이다.
    • 패턴 활용 방식: MEMENTO 패턴
    [그림 22-1] 같은 바둑판 객체에 대해 단순히 계산을 통해 무르기 기능을 수행하는 것은 때로 정상적으로 무르기를 수행하지 못하는 문제가 있다. 이런 문제가 일어나는 가장 원인은 [그림 22-1] 바둑판 객체가 무르기를 정상적으로 수행할 있을 만큼 충분한 정보를 가지고 있지 않다는데 있다. 예를 들어 board_[][] 이차원 배열은 사석으로 드러내진 돌에 대한 정보를 가지고 있지 않다는 있다. 예를 들어 board_[][] 이차원 배열은 사석으로 드러내진 돌에 대한 정보를 가지고 있지 않다. 또한 whiteDeadNum_, blackDeadNum_ 같이 사석 수를 나타내는 데이터 멤버나 패의 위치를 나타내는 paePosX_, paePosY_ 같은 데이터 멤버도 혀내의 사석 수나 위치 정보를 표현하고 있을 이전 바둑판에서의 사석 수나 위치 정보를 가지고 있지 않다. 따라서 아무리 복잡한 계산을 한다고 하더라도 완전히 이전 바둑판 상태로 무르기를 수행하는 것이 불가능한 것이다.
    우선 생각해볼 있는 방법은 [그림 22-1] 바둑판 객체 내부에 무르기 기능을 수행하기 위해 필요한 정보들을 모두 데이터 멤버로 추가하는 것이다. 예를 들어 사석을 포함해서 바둑돌이 놓여진 순서 위치 정보 리스트와 수순별로 사석 수나 패의 위치 정보 리스트를 데이터 멤버로 각각 추가하는 것이다. 이렇게 추가된 데이터 멤버에 대해 바둑판 객체는 바둑 돌이 놓여질 때마다 새로 놓여진 바둑돌의 위치 정보와 사석 , 패의 위치 등을 리스트에 추가해놓게 되면 무르기 요청이 발생하더라도 계산을 통해 이전 상태로 되돌아가는 것이 가능할 것이다.
    그러나 방법은 바둑판 객체의 입장에서 별도의 데이터 멤버를 추가로 관리해야 한다는 부담이 있을 뿐만 아니라 매번 바둑 돌이 놓여질 때마다 이들 데이터 멤버의 값을 갱신해주어야 하는 불편함이 있다. 또한 무르기가 요청되었을 경우에는 이들 데이터 멤버로부터 원하는 자료값을 일일이 찾아서 복잡한 처리를 해주어야 한다는 문제가 있다.
    [그림 22-1] 바둑판 객체가 가지고 있는 데이터 멤버를 살펴보면 어느 시점의 바둑판 상태 정보를 표현하고 있음을 있다. 그렇다면 이런 상태 정보를 바둑돌이 놓이는 순간마다 저장해서 리스트로 관리한다면 어떻게 될까? 아마도 우리는 쉽게 특정 시점의 바둑판 상태로 되돌아갈 있을 것이다. 이는 다시 말해 쉽게 무르기가 가능하다는 것을 의미한다. 왜냐하면 마지막 바둑돌이 놓여지기 전에 저장된 상태 정보를 되돌아가면 곧바로 무르기가 이루어진 것과 동일한 효과가 나타날 것이기 때문이다.
    이런 아이디어를 적용하려고 바둑판 객체의 상태 정보를 리스트로 저장, 관리할 있는 적당한 방법은 무엇일까?
    우선 생각할 있는 방법은 바둑판 객체가 가지고 있는 각각의 데이터 멤버에 대해 하나 하나 리스트를 정의해서 저장, 관리하는 것이다. 그러나 방법은 관리해야 리스트의 개수가 너무 많아지고, 특정 시점의 바둑판 상태 정보를 찾아내기 위해 리스트를 일일이 찾아다녀야 한다는 불편함이 존재한다.
    따라서 보다 나은 방법은 바둑판 객체가 가지고 있는 데이터 멤버를 하나의 클래스로 정의하고 클래스를 이용해서 바둑판 상태 정보를 시점별로 저장, 관리하는 리스트를 만들어 사용하도록 만드는 것이다. 이렇게 경우 하나의 리스트만으로 시점의 바둑판 상태 정보를 찾아다닐 있어 편리할 것이다.


    [그림 22-2]에서 GoBoard 클래스의 historyList_ 데이터 멤버는 바둑돌이 놓여질 때마다 그전까지의 바둑판 상태 정보를 GoMemento 객체 형태로 저장, 관리하기 위한 리스트다. 반면 pCurBoard_ 데이터 멤버는 현재의 바둑판 상태 정보를 GoMemento 객체 형태로 저장하고 있는 것이다. 이처럼 현재의 바둑판 상태 정보와 이전 바둑판 상태 정보가 동일한 자료형을 사용하기 때문에 RetractStone() 멤버 함수에 의해 무르기가 요청되더라도 historyList_ 데이터 멤버에서 가장 최근에 저장된 GoMemento 객체를 찾아 pCurBoard_ 복사하면 손쉽게 무르기 작업을 수행할 있게 된다. 또한 같은 클래스 구조를 이용하면 historyList_ 데이터 멤버의 항목들을 앞뒤로 따라다니면서 바둑돌이 놓여진 순서를 복기해볼 수도 있게 된다.
    • 샘플 코드
    [소스 22-1] 바둑판 상태 정보를 관리하는 바둑판 객체를 위해 Memento 패턴 적용
    #include <iostream>
    #include <iomanip>
    #include <list>
    #include <iterator>
    using namespace std;
     
    #define GO_BOARD_SIZE 19
     
    class GoMemento
    {
    friend class GoBoard;
    public:
    GoMemento()
    {
    for (int i = 0; i < GO_BOARD_SIZE; i++)
    for (int j = 0; j < GO_BOARD_SIZE; j++)
    board_[i][j] = 0;
     
    whiteDeadNum_ = blackDeadNum_ = 0;
    paePosX_ = paePosY_ = -1;
    }
     
    GoMemento(const GoMemento& rhs)
    {
    CopyBoard(rhs);
    }
     
    GoMemento& operator=(const GoMemento& rhs)
    {
    CopyBoard(rhs);
    }
     
    void GetOutDeadStone()
    {
    // 죽은 돌을 골라낸다.
    // whiateDeadNum_이나 blackDeadNum_값이 조정됨
    }
     
    bool IsPaePostion(int x, int y)
    {
    // x, y위치가 패이면 return true;
    return false;
    }
    protected:
    void CopyBoard(const GoMemento& src)
    {
    for (int i = 0; i < GO_BOARD_SIZE; i++)
    for (int j = 0; j < GO_BOARD_SIZE; j++)
    board_[i][j] = src.board_[i][j];
     
    whiteDeadNum_ = src.whiteDeadNum_;
    blackDeadNum_ = src.blackDeadNum_;
    }
     
    private:
    // 0은 돌없음, 양수은 백, 음수는 흑, 절대값은 수순
    int board_[GO_BOARD_SIZE][GO_BOARD_SIZE];
     
    int whiteDeadNum_;
    int blackDeadNum_;
     
    int paePosX_;
    int paePosY_;
    };
     
    class GoBoard
    {
    public:
    GoBoard(int firstTurn = -1)
    {
    pCurBoard_ = new GoMemento();
    whoseTurn_ = firstTurn;
    totalStoneNum_ = 0;
    }
     
    void PutStone(int x, int y)
    {
    if (pCurBoard_->board_[x][y] != 0
    || (pCurBoard_->paePosX_ == x && pCurBoard_->paePosY_ == y))
    {
    cout << "Can't Be Put Stone There" << endl;
    return;
    }
     
    GoMemento* pNewBoard = new GoMemento(*pCurBoard_);
    totalStoneNum_++;
    pNewBoard->board_[x][y] = whoseTurn_ * totalStoneNum_;
    whoseTurn_ *= -1;
    if (pCurBoard_->IsPaePostion(x, y))
    {
    pCurBoard_->paePosX_ = x;
    pCurBoard_->paePosY_ = y;
    }
    else
    {
    pCurBoard_->paePosX_ = -1;
    pCurBoard_->paePosY_ = -1;
    }
     
    pNewBoard->GetOutDeadStone();
     
    historyList_.push_front(pCurBoard_);
    pCurBoard_ = pNewBoard;
    }
     
    void RetractStone(int cnt)
    {
    // cnt값만큼 수를 무른다
    if (cnt <= 0)
    return;
     
    for (int i = 0; i < cnt - 1; i++)
    {
    GoMemento* pTmpBoard = historyList_.front();
    delete pTmpBoard;
    historyList_.pop_front();
    totalStoneNum_--;
    }
     
    delete pCurBoard_;
    totalStoneNum_--;
    if (historyList_.empty())
    pCurBoard_ = new GoMemento();
    else
    pCurBoard_ = historyList_.front();
    }
     
    void PrintBoard()
    {
    for (int i = 0; i < GO_BOARD_SIZE; i++)
    {
    for (int j = 0; j < GO_BOARD_SIZE; j++)
    {
    cout << pCurBoard_->board_[i][j] << " ";
    }
    cout << endl;
    }
    cout << "---total stone---" << totalStoneNum_ << endl;
    }
     
    private:
    list<GoMemento*> historyList_;
    GoMemento* pCurBoard_;
    int whoseTurn_;
    int totalStoneNum_;
    };
     
    void main()
    {
    GoBoard board;
     
    board.PutStone(3, 3);
    board.PutStone(16, 16);
    board.PutStone(16, 3);
    board.PutStone(3, 16);
    board.PrintBoard();
     
    board.RetractStone(2);
    board.PrintBoard();
    }
    • 구현 관련 사항
    Mement 클래스는 가지 범주의 인터페이스를 가지게 하는 것이 좋다. 하나는 Memento 클래스 내부의 정보를 모두 접근 가능하도록 넓게 공개된 인터페이스고, 다른 하나는 규정된 인터페이스를 통해서만 Memento 클래스 객체를 접근할 있도록 하는 것이다. 이때 전자는 주로 Memento 클래스 내부의 정보를 자신의 상태 정보로 사용하는 클래스를 위한 것이며, 후자는 일반적인 클래스를 위한 것이다. C++ 프로그래밍 언어에서는 friend 선언을 통해 특정 클래스에게 Memento 클래스의 내부를 모두 공개할 있으며, 일반 클래스들에게는 public 형태로만 인터페이스를 공개할 있어 가지 범주의 인터페이스 정의가 용이하다.
    Memento 패턴의 구현에서 주로 고민하게 되는 것은 Memento 객체를 생성하는데 많은 비용이 드는 경우 어떻게 것인가 하는 점이다. 특히 Memento 객체가 포함해야 상태 정보가 많거나 자주 Memento 객체가 생성되어야 경우에는 이런 문제가 더욱 심각할 있다. 같은 경우 한가지 해결 방안은 새로 생성되는 Memento 객체는 바로 직전에 생성된 Memento 객체에서 변경된 부분만을 저장하게 만드는 것이다. 다만 이렇게 경우 바둑판을 다시 그릴려면 historyList_ 저장된 정보를 처음부터 읽어서 바둑 돌을 놓아야 하고, 바둑 돌이 놓여지면서 사석이 돌은 계산해서 빼내야 하는 번거로운 작업이 많을 있다. 그리고 여러 무르기를 경우 무르는 형태로 진행해야 하는 단점이 있다.
    • MEMENTO 패턴 정리


    [그림 22-3]에서 Originator 클래스는 CreateMemento() 멤버 함수가 불릴 경우, 시점에서 자신의 상태 정보를 Memento 객체로 설정해서 되돌리는 역할을 한다. 따라서 이를 이용하면 Creataker 같은 클래스 객체는 자신이 원하는 시점의 Originator 객체 상태를 Memento 객체로 되돌려받아 저장, 관리할 있게 된다. 또한 Originator 클래스는 SetMemento() 인터페이스를 제공하는데 이는 CreateMement() 멤버 함수에 의해 생성된 Memento 객체를 이용해서 Originator 객체 상태를 복원하고 싶을 사용하기 위한 것이다.
    Memento패턴이 유용한 경우
    • 어떤 객체의 특정 시점에 대한 상태 정보가 나중에 복원되기 위해 저장되어야 경우
    • 객체의 상태를 직접 접근하는 것이 해당 객체의 구현 너무 복잡하거나 정보 은닉을 깨뜨릴 경우
    Memento 패턴의 장단점, 다른 패턴들과의 연관 관계
    • Client Originator 객체의 내부 상태를 저장, 관리해야 경우 이를 직접 접근하는 것을 막아준다. 따라서 Memento 패턴은 Originator 객체의 내부가 다른 객체에 의해 직접적으로 영향을 받지 않게 경계 역할을 수행해준다고 있다.
    • Memento 패턴을 적용하지 않을 경우 Originator 객체는 자신의 상태 정보를 Client 원하는 시점마다 관리하고 있어야 한다. 왜냐하면 Client 특정 시점으로 복귀를 원할 경우 이를 수행해줄 있어야 하기 때문이다. Client 직접 Originator 객체의 상태 정보를 접근해서 수정하게 하면 되겠지만 이는 Originator 객체의 정보 은닉이 깨어지는 것이기 때문에 좋지 않다.
    반면 Memento패턴을 적용할 경우 Client 스스로 자신이 원하는 시점의 Originator 객체 상태 정보를 Memento 객체를 이용해서 저장, 관리할 있으므로, Originator 객체의 내부 구현이 훨씬 간단해질 있다.
    • Memeno 객체의 생성은 Originator 객체의 내부 상태를 모두 복사해야 하는 비용을 포함하고 있다. 따라서 Memento 객체의 생성이 자주 일어나거나 Originator 객체의 내부 상태 정보가 많은 경우에는 Memento 패턴의 적용이 부적절할 있다. 따라서 Memento 패턴의 적용 여부는 비용대비 효용을 판단해서 결정해야 한다.
    • Memento 객체의 저장이나 관리, 삭제는 Client 알아서 수행해야 한다. 그런데 문제는 Client 입장에서는 Memento 객체에 얼마나 많은 상태 정보가 저장되어 있는지를 있는 방법이 없다. 따라서 Client Memento 객체가 얼마나 많은 저장 공간을 사용하는지를 판별할 없으며, 이로 인해 생각 이상으로 많은 저장 공간이 소모되고 있을 있다. 그러므로 Client 저장, 관리할 Memento 객체의 개수를 적당히 조절할 필요가 있다.
    • Memento 패턴은 Command 패턴과 함께 자주 사용될 있다. 이때 Memento 패턴은 Command 패턴에서 작업이 수행되기 전의 상태 정보를 저장해두는 역할을 담당하게 된다. 이를 통해 Memento 패턴은 Command 패턴에 의해 수행했던 작업을 갑자기 취소하고자 작업이 수행되기 이전 상태로 쉽게 복귀할 있게 도와주는 역할을 하게 된다.
    • Iterator 패턴의 구현 시에도 사용될 있다. Memento 객체는 Iterator 클래스 내부에서 현재 항목의 위치 정보를 저장, 관리하는 목적으로 사용될 있다.
반응형