본문 바로가기

유니티/디자인 패턴

[Unity] 메멘토 패턴 (Memento Pattern)

반응형

메멘토 패턴이란?

우리가 문서 작업을 할 때 실수로 오타를 내거나 위치를 잘못 옮기면 어떻게 하는가?

혹은 모델링 작업에서 매쉬를 잘못 변경했다면?

 

가장 간단한 방법은 Ctrl+Z를 눌러서 이전 상태로 복구하는 방법일 것이다.

우리는 간편하게 단축키로 이전 상태로 복구하지만, 이전 상태로 돌아가기 위해서는 이전 상태가 어떤 것인지 컴퓨터 메모리에 저장되어 있어야 한다.

 

메멘토 패턴은 이런 상태 저장과 관련된 디자인 패턴이다.

따라서 이번 예제에서도 상태 저장 및 복구와 관련하여 포스팅하였다.

 

구조는 크게 CareTaker와 Originator, Memento로 나뉜다.

 

메멘토 패턴 구조

 

Memento 객체는 특정 상태를 담기 위한 객체이다. 어떤 순간의 정보가 이 Memento 객체에 담긴다.

Originator는 Memento 객체를 생성하고 불러올 수 있다.

Caretaker는 여러 개의 Memento들을 Originator를 통해 관리한다.


왜 사용하는 것일까?

메멘토 패턴을 사용하면 특정 객체의 상태를 캡슐화를 유지하며 저장할 수 있다는 장점이 있다. 이번 예제에서도 캡슐화와 은닉성을 유지하기 위해 클래스 내부에 Memento 클래스를 작성할 것이다. (만약 상태를 저장하기 위해 별도의 구조체나 클래스를 만든다면 캡슐화가 어렵다.)

 

장점

  1. 캡슐화 : 객체 내부의 상태를 캡슐화하여 안전하게 저장 및 복원 가능
  2. 은닉성 : 객체 내부의 상태를 외부에 노출하지 않기 때문에 상태 관리에 유연함을 제공

단점

  1. 성능 오버헤드 : 잘못 사용할 경우 데이터를 저장하기 위해 과도한 메모리를 사용할 가능성이 있고 이는 성능 오버헤드로 이어짐
  2. 복잡성 : 객체의 상태가 아주 복잡할 경우 이를 저장하기 위한 메멘토 패턴 역시 복잡해질 수 있고 이는 가독성과 유지보수의 하락으로 이어지는 문제 발생

어떻게 사용할 수 있을까?

처음 메멘토 패턴이 Caretaker와 Originator, Memento라는 세 가지 객체로 이루어진다는 것을 기억하자.

 

이번 예시에서는 유니티로 게임을 제작할 때 인벤토리 슬롯의 상태에 대한 정보를 저장하고 되돌리는 기능을 예제로 작성하였다. 이 예제에서는 여러 개의 아이템들이 있고, 이 아이템들의 종류와 순서 등을 인벤토리가 갖고 있으며, 컨트롤러라는 클래스가 이 정보를 불러오거나 저장한다.

따라서 관리의 대상인 Item클래스와 아이템을 담을 Inventory클래스가 있으며, 이를 관리하는 Controller라는 클래스를 추가하였다.

 

정리하자면 아래 표와 같다.

 

메멘토 패턴 객체 이름 예제 클래스 이름 역할
Caretaker Controller 메멘토들을 저장하거나 불러와서 상태를 변경하는 객체
Originator Inventory 현재의 인벤토리 상태, 메멘토를 생성하고 불러오는 객체
Memento Memento 상태를 저장하는 객체 단위
- Item 아이템 자체

 

먼저 아이템 클래스이다.

 

[CreateAssetMenu(fileName = "Item", menuName = "DesignPattern/MementoPattern/Item")]
public class Item : ScriptableObject
{
    public string Name;
}

 

스크립터블 오브젝트로 만들어 외부에서 참조가 가능하도록 한다.

 

아래는 이러한 아이템들을 담을 인벤토리 클래스와 메멘토 클래스이다.

 

//this is Originator
public class Inventory : MonoBehaviour
{
    public List<Item> inventoryItems = new();

    public Memento CreateMemento()
    {
        var newMemento = new Memento();
        newMemento.SetState(inventoryItems);
        return newMemento;
    }

    public void RestoreMemento(Memento memento)
    {
        inventoryItems = memento.GetState();
    }

    public class Memento
    {
        private List<Item> Items;

        public List<Item> GetState()
        {
            return Items;
        }

        public void SetState(List<Item> items)
        {
            Items = new(items);
        }
    }
}

 

이렇게 Memento 클래스를 Inventory 클래스의 내부 클래스로 작성하여 캡슐화를 하도록 한다. 필요시에 SetState 대신 생성자에서 바로 리스트를 만들고 Items를 읽기 전용으로 만들어 Immutable 클래스로 작성할 수도 있다.

 

이제 이러한 메멘토들을 불러오거나 필요시에 저장할 Controller 클래스이다.

 

//this is Caretaker
public class Controller : MonoBehaviour
{
    public Inventory inventory;
    public Button saveButton;
    public Button cancelButton;

    private Stack<Inventory.Memento> mementos = new();

    void Start()
    {
        saveButton.onClick.AddListener(Save);
        cancelButton.onClick.AddListener(Cancel);
    }

    private void Save()
    {
        var newMemento = inventory.CreateMemento();
        mementos.Push(newMemento);
    }

    private void Cancel()
    {
        if (mementos.Count <= 0)
            return;

        var memento = mementos.Pop();
        inventory.RestoreMemento(memento);
    }
}

 

Stack으로 메멘토들을 저장한다. 예시에서는 Stack을 사용했지만 특정 순간으로 지정해서 변경하는 것이라면 List나 배열을 사용해도 당연히 무방하다.

 

이제 컨트롤러 클래스와 인벤토리 클래스 오브젝트를 각각 만들어주고, 인벤토리에는 Scriptable Object로 생성된 아이템들을 미리 몇 개 넣어둔다.

 

 

아래 이미지는 저장과 취소(불러오기)를 할 수 있는 버튼들이다.

 

 

이제 플레이모드에서 인벤토리의 조합이나 순서를 변경한 후 저장을 몇 번 한 다음에 취소를 하면 이전 상태로 되돌아오는 것을 확인할 수 있다.

 

메멘토 패턴으로 생성된 상태 저장 기능

 

아래는 위의 코드에 대한 UML 다이어그램이다.

반응형