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 |