개요
MS오피스 같은 문서편집기나 다른 여러 컴퓨터 프로그램에서 자연스럽게 익숙해진 기능이 바로 되돌리기 기능이다. 객체지향 프로그램상에서 이러한 되돌리기 기능이 있다면 어떨까? 실행 이전 상태로 되돌려야 하기 때문에 인스턴스가 갖고 있는 정보를 일정기간 저장해놓아야 하고, 인스턴스 상태를 실행 이전으로 되돌려야 한다. 그런데 외부에서 인스턴스를 조작해서 상태를 조작하는 것은 객체지향 프로그래밍에서 중요한 기법인 캡슐화와는 반대되는 방법이다.
캡슐화를 파괴하지 않으면서도 정보 저장과 상태 복원이 가능하도록 해주는 것이 바로 Memento(기념품) 패턴이다.
Memento 패턴을 이용해 undo, redo, history, snapshot 등의 기능을 구현할 수 있다.
역할
Originator 역할
자신의 현재 상태를 저장하고 싶을 때 Memento 역할의 인스턴스를 만들고, 이전의 상태로 되돌리고 싶을 땐 Mememto 역할의 인스턴스를 받아서 교체함으로 상태를 되돌린다. 예제에서는 Gamer 클래스.
Memento 역할
Originator의 내부 정보를 정리하서 저장하고 있으며 외부로 정보를 노출시키지 않는다. 예제에서는 Memento 클래스.
Caretaker 역할
현재 상태를 저장하고 싶을 때 Originator 역할에게 저장하라는 메시지를 전달하고 Originator는 메시지를 받아 Memento 역할의 인스턴스를 생성해 Caretaker에게 전달한다. Caretaker는 백업이 필요할 때를 대비해 Memento 인스턴스를 보관하고 있는다. 예제에서는 MemetoTester 클래스.
이득 및 주의사항
Memento 패턴에는 java의 접근제어자를 이용해서 캡슐화가 파괴되지 않게 하고 있다. Caretaker는 Memento 인스턴스를 보관하지만 getMoney() 메서드를 제외하고는 해당 인스턴스를 이용할 수 없다.
Caretaker가 보관하는 Memento 인스턴스 갯수를 늘려서 여러 시점의 상태를 저장할 수 도 있다. 문제는 Memento 인스턴스를 DB 등에 보관할 때 인데, 만약 프로그램의 버전이 변경되거나 수정되면 이전에 저장하고 있던 Memento 인스턴스를 사용할 때 모순이 발생할 수 도 있기에 주의해야 한다.
Originator(예제의 Gamer 클래스)에 undo나 snapshot 기능을 포함시키면 간단하겠지만 이를 굳이 분리하는 이유는 이를 통해 다양한 버전의 undo 정보를 저장하거나 Originator의 현재 상태를 파악하는 등에 있어서 Originator 역할의 클래스를 수정할 필요가 없다는 것이다. Originator는 다양한 버전(undo를 위한 이전상태)과 상관없이 현재 정보만 가지고 있으면 되는 것이다.
예제 코드
//Originator 역할
package memento.game;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
public class Gamer {
private int money;
private List 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) {
money +=100;
System.out.println("소지금 증가");
} else if(dice==2) {
money /=2;
System.out.println("소지금 반");
} else if(dice == 6) {
String f= getFruit();
System.out.println("과일 (" + f + ")을 받았습니다.");
fruits.add(f);
} else {
System.out.println("변함 없음");
}
}
public Memento createMemento() {
Memento m = new Memento(money);
Iterator it = fruits.iterator();
while(it.hasNext()) {
String f = (String)it.next();
if(f.startsWith("맛있는")) {
m.addFruit(f);
}
}
return m;
}
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 + fruitsname[random.nextInt(fruitsname.length)];
}
}
//Memento 역할
package memento.game;
import java.util.ArrayList;
import java.util.List;
public class Memento {
int money;
ArrayList fruits;
public int getMoney() {
return money;
}
Memento(int money) {
this.money = money;
this.fruits = new ArrayList();
}
void addFruit(String fruit) {
fruits.add(fruit);
}
List getFruits() {
return (List)fruits.clone();
}
}
//Caretaker역할
package memento;
import memento.game.Gamer;
import memento.game.Memento;
public class MementoTester {
public static void main(String[] args) {
Gamer gamer = new Gamer(100);
Memento memento = gamer.createMemento();
for(int i=0; i<100; 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()) {
System.out.println("감소 : 이전 상태로 되돌리기");
gamer.restoreMemento(memento);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("");
}
}
}
}
'디자인 패턴' 카테고리의 다른 글
21. Proxy 패턴 (0) | 2021.01.11 |
---|---|
20. Flyweight 패턴 (0) | 2021.01.05 |
17. Observer 패턴 (0) | 2020.12.29 |
16. Mediator 패턴 (0) | 2020.12.28 |
15. Facade 패턴 (0) | 2020.12.26 |