Design Pattern | Memento Pattern (메멘토 패턴)

Memento 패턴이란?

  • Memento 라는 영어 단어는 기념품, 모양, 추억의 종이라는 의미이다.
  • 객체 지향 프로그램에서의 실행 취소(Undo)를 하려면, 인스턴스가 가지고 있는 정보를 저장해야 한다.
  • 인스턴스를 복원하려면 인스턴스 내부의 정보에 자유롭게 액세스할 수 있어야 한다. 그러나, 부주의하게 액세스를 허가해 버리면, 그 클래스의 내부 구조에 의존한 코드가 되어 버린다. 이를 캡슐화의 파괴라고 한다.
  • Memento 패턴은 인스턴스의 상태를 나타내는 역할을 도입하여, 캡슐화의 파괴에 일으키지 않고 상태(이전의 인스턴스)를 저장/복원을 수행하는 방식이다.
  • GoF의 디자인 패턴에서는 행위에 대한 디자인 패턴으로 분류된다.

Memento 패턴 예제 프로그램

던지 주사위의 수에 따라 소지금과 소지품(과일)을 변화시키는 프로그램이다.
상황에 따라 저장 및 제거를 수행한다.

Class Diagram
Memento Pattern Class Diagram

1. Memento 클래스

Game의 상태를 나타내는 클래스이다.

Memento.java

package com.devkuma.designpattern.behavioral.memento.game;

import java.util.ArrayList;

public class Memento {

    int money;
    ArrayList<String> fruits;

    public int getMoney() {
        return money;
    }

    Memento(int money) {
        this.money = money;
        this.fruits = new ArrayList();
    }

    void addFruit(String fruit) {
        fruits.add(fruit);
    }

    ArrayList<String> getFruits() {
        return (ArrayList<String>) fruits.clone();
    }
}

위에 코드에서는 변수나 함수에 모두 public, private와 등 접근 제안자를 선언하지 않았다. 이는 game 이외의 패키지에서는 내부를 변경할 수 없다는 것을 뜻한다.

2. Gamer 클래스

Game을 하는 주인공의 클래스이다. Memento의 인스턴스를 만든다.

Gamer.java

package com.devkuma.designpattern.behavioral.memento.game;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.Random;

public class Gamer {

    private int money;
    private ArrayList<String> fruits = new ArrayList();
    private Random random = new Random();
    private static String[] fruitNames = {
            "사과", "포도", "바나나", "귤",
    };

    public Gamer(int money) {
        this.money = money;
    }

    public int getMoney() {
        return money;
    }

    // 베팅...게임 진행한다.
    public void bet() {
        // 주사위를 던진다.
        int dice = random.nextInt(6) + 1;

        if (dice == 1) {
            // 1인 경우, 소지금 증가한다.
            money += 100;
            System.out.println("소지금이 증가하였습니다.");
        } else if (dice == 2) {
            // 2인 경우, 소지금이 절반이 된다.
            money /= 2;
            System.out.println("소지금이 절반이 되었습니다.");
        } else if (dice == 6) {
            // 6인 경우, 과일을 받는다.
            String fruit = getFruit();
            System.out.println("과일(" + fruit + ")을 받았습니다.");
            fruits.add(fruit);
        } else {
            // 그외인 경우, 아무 일도 일어나지 않는다.
            System.out.println("아무 일도 일어나지 않았다.");
        }
    }

    // 스냅샷 생성: 현재 상황을 저장한 객체를 생성하고 반환한다.
    public Memento createMemento() {
        Memento memento = new Memento(money);
        Iterator it = fruits.iterator();
        while (it.hasNext()) {
            String fruit = (String) it.next();
            if (fruit.startsWith("맛있는")) {
                // 맛있는 과일만 저장한다.
                memento.addFruit(fruit);
            }
        }
        return memento;
    }

    // 실행 취소(Undo)을 실행: 저장했던 객체를 전달받아 이전 상태로 되돌린다.
    public void restoreMemento(Memento memento) {
        this.money = memento.money;
        this.fruits = memento.getFruits();
    }

    public String toString() {
        return "[money = " + money + ", fruits = " + fruits + "]";
    }

    private String getFruit() {
        String prefix = "";
        if (random.nextBoolean()) {
            prefix = "맛있는 ";
        }
        return prefix + fruitNames[random.nextInt(fruitNames.length)];
    }
}

3. Main 클래스

메인 처리를 실행하는 클래스이다. 게임을 진행시킨다. 또한 Memento의 인스턴스를 저장하고 필요에 따라 Gamer의 상태를 복원한다.

Main.java

package com.devkuma.designpattern.behavioral.memento;

import com.devkuma.designpattern.behavioral.memento.game.Gamer;
import com.devkuma.designpattern.behavioral.memento.game.Memento;

public class Main {

    public static void main(String[] args) {
        // 첫 소지금은 100이다.
        Gamer gamer = new Gamer(100);
        // 첫번째 상태를 저장한다.
        Memento memento = gamer.createMemento();

        for (int i = 0; i < 10; i++) {
            System.out.println("==== " + i);
            System.out.println("현재 상태:" + gamer);

            // 게임을 진행한다.
            gamer.bet();

            System.out.println("소지금은 " + gamer.getMoney() + "원이 되었습니다.");

            if (gamer.getMoney() > memento.getMoney()) {
                System.out.println("    (많이 증가했으므로 현재 상태를 저장하자)");
                memento = gamer.createMemento();
            } else if (gamer.getMoney() < memento.getMoney() / 2) {
                System.out.println("    (많이 줄어들었으므로 이전 상태로 복원하자)");
                gamer.restoreMemento(memento);
            }
        }
    }
}

4. 실행 결과

==== 0
현재 상태:[money = 100, fruits = []]
소지금이 증가하였습니다.
소지금은 200원이 되었습니다.
    (많이 증가했으므로 현재 상태를 저장하자)
==== 1
현재 상태:[money = 200, fruits = []]
소지금이 절반이 되었습니다.
소지금은 100원이 되었습니다.
==== 2
현재 상태:[money = 100, fruits = []]
아무 일도 일어나지 않았다.
소지금은 100원이 되었습니다.
==== 3
현재 상태:[money = 100, fruits = []]
아무 일도 일어나지 않았다.
소지금은 100원이 되었습니다.
==== 4
현재 상태:[money = 100, fruits = []]
아무 일도 일어나지 않았다.
소지금은 100원이 되었습니다.
==== 5
현재 상태:[money = 100, fruits = []]
아무 일도 일어나지 않았다.
소지금은 100원이 되었습니다.
==== 6
현재 상태:[money = 100, fruits = []]
과일(맛있는 귤)을 받았습니다.
소지금은 100원이 되었습니다.
==== 7
현재 상태:[money = 100, fruits = [맛있는 귤]]
과일(맛있는 사과)을 받았습니다.
소지금은 100원이 되었습니다.
==== 8
현재 상태:[money = 100, fruits = [맛있는 귤, 맛있는 사과]]
과일(사과)을 받았습니다.
소지금은 100원이 되었습니다.
==== 9
현재 상태:[money = 100, fruits = [맛있는 귤, 맛있는 사과, 사과]]
아무 일도 일어나지 않았다.
소지금은 100원이 되었습니다.

Memento 패턴의 장점

Memento 패턴을 사용하면 실행 취소(Undo), 다시 실행(Redo), 작업 이력 작성, 현재 상태 저장 등을 할 수 있다.
실행 취소(Undo)를 하고 싶다면, Gamer 클래스에 그 기능을 만들어 넣으면 좋을까 하는 의문도 나올 수 있을 거라 생각된다.
Main 클래스에서는, “어느 타이밍에 스냅샷을 찍을까”, “언제 실행 취소를 할지"를 결정하여 Memento를 보관 유지하는 일을 실행한다.
한편, Gamer 클래스에서는 Memento를 만드는 작업과 주어진 Memento를 사용해 자신의 상태를 되돌리는 일을 실행한다.
Main 클래스와 Gamer 클래스에서는 이와 같이 역할 분담을 하고 있는 것을 알 수 있다. 이렇게 역할 분담을 하게 되면,

  • 여러 단계를 취소하도록 변경하고 싶다.
  • 실행 취소뿐만 아니라 현재 상태를 파일에 저장하고 싶다. 와 같은 수정 사항을 반영하고 하고 싶을 때에도 Gamer를 변경할 필요는 없어진다.