본문 바로가기

Language/Java

Part 18. Memento

18. Memento 패턴

- 이전의 상태를 누군가가 기억하고 있어야 한다

- 현재 객체의 상태를 기록하여 보존하기 위한 Memento 패턴

- 객체 내부 상태를 최대한 공개하지 않고 객체의 상태를 저장하는 방법을 제공함

● 클래스 다이어그램

※ Memento 클래스 (Memento의 역할)

- 게이머의 상태를 표현하는 클래스

- Originator 역할의 내부 정보를 보존한다

- 생성자에 public이 없다 : 같은 패키지에 속하는 클래스에서만 호출할 수 있다

package ch18.A4.game;

import java.io.*;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.List;

public class Memento {
    private int money;             // 소지금 
    private List<String> fruits;  // 과일

    // 소지금을 얻는다(narrow interface)
    public int getMoney() {
        return money;
    }

    // 생성자(wide interface)
    Memento(int money) {
        this.money = money;
        this.fruits = new ArrayList<>();
    }

  // 과일을 추가한다(wide interface)
    void addFruit(String fruit) {
        fruits.add(fruit);
    }

    // 과일을 얻는다(wide interface)
    List<String> getFruits() {
        return new ArrayList<>(fruits);
    }

    // 파일에 저장
    public static boolean saveToFile(String filename, Memento memento) {
        StringBuilder sb = new StringBuilder();

        // 소지금
        sb.append(String.format("%d", memento.money));
        sb.append("\n");

        // 과일
        for (String f: memento.getFruits()) {
            sb.append(f);
            sb.append("\n");
        }

        // 쓰기
        try {
            Files.writeString(Path.of(filename), sb,
                    StandardOpenOption.CREATE,
                    StandardOpenOption.TRUNCATE_EXISTING,
                    StandardOpenOption.WRITE);
        } catch (IOException e) {
            System.out.println(e.toString());
            return false;
        }
        return true;
    }

    // 파일로부터 생성
    public static Memento loadFromFile(String filename) {
        try {
            // 읽기
            List<String> lines = Files.readAllLines(Path.of(filename));
            if (lines.size() == 0) {
                System.out.println("Empty file");
                return null;
            }

            // 소지금
            int money = 0;
            try {
                money = Integer.parseInt(lines.get(0));
            } catch (NumberFormatException e) {
                System.out.println("Format error: " + e);
                return null;
            }

            // 생성
            Memento memento = new Memento(money);

            // 과일
            for (int i = 1; i < lines.size(); i++) {
                memento.addFruit(lines.get(i));
            }
            return memento;
        } catch (IOException e) {
            System.out.println(e.toString());
            return null;
        }
    }
}

※ Gamer 클래스(Originator의 역할)

- 게임을 수행하는 게이머를 표현하는 클래스

- Memento 역할을 생성하는 일을 함

- Memento 역할을 만들어 자신의 현재 상태를 반환하는 역할을 한다

- 또한, Memento로부터 이전 상태를 받아서 복귀하는 일을 한다.

package ch18.A4.game;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class Gamer {
    // 소지금
    private int money;
    // 과일 
    private List<String> fruits = new ArrayList<>();
    // 난수 생성기 
    private Random random = new Random();

    // 과일 이름 표 
    private static String[] fruitsName = {
        "사과", "포도", "바나나", "오렌지",
    };

    // 생성자 
    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 f = getFruit();
            System.out.println("과일(" + f + ")를 받았습니다.");
            fruits.add(f);
        } else {
            // 그 밖의 눈 … 아무일도 일어나지 않는다 
            System.out.println("변동 사항이 없습니다.");
        }
    }

    // 스냅샷을 찍는다 
    public Memento createMemento() {
        Memento m = new Memento(money);
        for (String f: fruits) {
            // 과일은 맛있는 것만 저장한다
            if (f.startsWith("맛있는 ")) {
                m.addFruit(f);
            }
        }
        return m;
    }

    // 복원한다 
    public void restoreMemento(Memento memento) {
        this.money = memento.getMoney();
        this.fruits = memento.getFruits();
    }

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

    // 과일을 하나 얻는다
    private String getFruit() {
        String f = fruitsName[random.nextInt(fruitsName.length)];
        if (random.nextBoolean()) {
            return "맛있는 " + f;
        } else {
            return f;
        }
    }
}

※ Main 클래스 (Caretaker의 역할)

- gamer.createMemento( )를 호출하여 현재 gamer의 상태를 얻어와서 보존한다.

- Originator 역할의 상태를 보존하고 싶을 때 Originator에게 알리는 역할을 담당한다

- Memento 역할을 보관하는 일을 함

package ch18.A4;

import ch18.A4.game.Memento;
import ch18.A4.game.Gamer;

import java.io.*;

public class Main {
    public static final String SAVEFILENAME = "game.dat";

    public static void main(String[] args) {
        Gamer gamer = new Gamer(100);         // 최초 소지금은 100

        // 파일에서 읽어온다
        Memento memento = Memento.loadFromFile(SAVEFILENAME);
        if (memento == null) {
            System.out.println("새로 시작합니다.");
            memento = gamer.createMemento();  // 최초 상태를 저장해 둔다
        } else {
            System.out.println("이전에 저장한 결과부터 시작합니다.");
            gamer.restoreMemento(memento);
        }

        // 게임 시작 
        for (int i = 0; i < 100; i++) {
            System.out.println("==== " + i);         // 횟수 표시 
            System.out.println("상태:" + gamer);    // 현재 주인공의 상태 표시

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

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

            // Memento 다루기로 결정 
            if (gamer.getMoney() > memento.getMoney()) {
                System.out.println("※많이 늘었으니 현재 상태를 저장해 두자!");
                memento = gamer.createMemento();
                // 파일로 기록한다 
                if (Memento.saveToFile(SAVEFILENAME, memento)) {
                    System.out.println("현재 상태를 파일로 저장했습니다.");
                }
            } else if (gamer.getMoney() < memento.getMoney() / 2) {
                System.out.println("※많이 줄었으니 이전 상태를 복원하자!");
                gamer.restoreMemento(memento);
            }

            // 시간 대기 
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            System.out.println();
        }
    }
}

❑ Originator 역할과 Memento 역할은 견고하게 결속되어 있다.

– Originator와 Memento는 같은 패키지에 속하고,

– Originator가 Memento 내부 정보에 쉽게 접근할 수 있다.

 

❑ Caretaker 역할과 Memento 역할은 느슨하게 연결되어 있다.

– Caretaker 역할이 Memento 내부의 정보를 쉽게 접근할 수 없다. => 캡슐화 유지(데이터를 안으로 숨긴다)

▶ 두개의 인터페이스(API)와 액세스 제어

▪ 좁은 인터페이스(Narrow Interface)

– 객체 내부 정보의 일부분만을 외부에 공개하는 인터페이스

– 예: Memento의 getMoney()

 

▪ 넓은 인터페이스(Wide Interface)

– 객체의 상태를 되돌리는 데 필요한 정보를 모두 얻을 수 있는 메소드 집합

– 객체의 내부 상태를 드러냄 – 예: Memento의 생성자, getMoney(), addFruit()

 

'Language > Java' 카테고리의 다른 글

Part 21. Proxy  (0) 2023.06.06
Part 7. Build  (0) 2023.06.06
Part 15. Facade  (0) 2023.06.05
Part 14. Chain of Responsibility  (0) 2023.06.05
Part 22. Command  (0) 2023.06.04