Memento Pattern

프로그래밍/Design Pattern 2012.01.26 04:06 Posted by 스코티
텍스트 에디터를 사용할 때, 필요한 텍스트를 실수로 삭제했어도 'undo'라는 기능을 사용하면 삭제하기 전의 상태로 텍스트를 복원할 수 있다. undo 기능을 몇 단계 실행해서 편집작업을 점점 과거로 되돌리는 에디터도 있다.

오브젝트 지향의 프로그램에서 undo 기능을 실행하려면 인스턴스가 가지고 있는 정보를 저장해 둘 필요가 있다. 단, 저장만 해서는 쓸모가 없고 저장한 정보로부터 인스턴스를 원래의 상태로 되돌려야 한다.

인스턴스를 복원하기 위해서는 인스턴스 내부의 정보를 자유롭게 액세스할 수 있어야 한다. 그러나 원하지 않는 액세스를 허용하면 클래스 내부 구조에 의존한 코드가 프로그램의 여기저기로 흩어져 클래스의 수정을 어렵게 만든다. 이것을 캡슐화의 파괴라고 한다. 인스턴스의 상태를 나타내는 역할을 도입해서 캡슐화의 파괴에 빠지지 않고 저장과 복원을 실행하는 것이 Memento Pattern이다.

Memento는 '기념물', '유품', '추억거리'라는 의미이다. 서랍 속에서 우연히 '추억의 사진'을 찾았다고 하자. 그리고 사진을 보면 그 당시의 기억이 생각나며 마치 과거로 돌아간 듯한 느낌이 들 것이다. Memento Pattern은 이와 유사한 패턴이다. 어떤 시점의 인스턴스의 상태를 확실하게 기록해서 저장해 두면 나중에 인스턴스를 그 시점의 상태로 되돌릴 수 있다.

다음은 Memento Pattern 클래스 다이어그램이다.

                                             [Memento Pattern Class Diagram]

Originator의 역할
- Originator 역할은 자신의 현재 상태를 저장하고 싶을 때 Memento 역할을 만든다. 또한, Originator 역할은 이전의 Memento 역할을 전달받으면 그 Memento 역할을 만든 시점의 상태로 돌리는 처리를 실행한다.

Memento의 역할
- Memento 역할은 Originator 역할의 내부 정보를 정리한다. Memento 역할은 Originator 역할의 내부 정보를 가지고 있지만, 그 정보를 누구에게도 공개하지 않는다. Memento 역할은 다음 두 종류의 인터페이스를 가지고 있다.

1. wide interface : Memento 역할이 제공하는 이 인터페이스는 오브젝트의 상태를 원래의 상태로 돌리기 위해 필요한 정보를 모두 얻을 수 있는 메소드의 집합이다. 이 인터페이스는 Memento 역할의 내부상태를 속속들이 들어내기 때문에 이것을 사용하는 것은 Originator 역할 뿐이다.
2. narrow interface : Memento 역할이 제공하는 이 인터페이스는 외부의 Caretaker 역할에게 보여주는 것이다. 이 인터페이스로 할 수 있는 일에는 한계가 있고 내부 상태가 외부에 공개되는 것을 방지한다.

이 두 종류의 인터페이스를 구별해서 사용하면 오브젝트의 캡슐화가 파괴되는 것을 방지할 수 있다.

Caretaker의 역할
- Caretaker 역할은 현재의 Originator 역할의 상태를 저장하고 싶을 때, 그것을 Originator 역할에게 전한다. Originator 역할은 그것을 받아서 Memento 역할을 만들어 Caretaker 역할에게 전달한다. Caretaker 역할은 미래의 필요에 대비해서 그 Memento 역할을 저장해 둔다.
Caretaker 역할은 Memento 역할이 갖는 2종류의 인터페이스중에서 narrow interface만 사용할 수 있으므로 Memento 역할의 내부 정보에 액세스할 수 없다.


예제를 한번 보자.
우선 예제 클래스 다이어그램은 다음과 같다.


- Memento.java
  1 package Memento_pattern_game;
  2
  3 import java.util.ArrayList;
  4 import java.util.List;
  5
  6 public class Memento {
  7     int money;
  8     ArrayList<String> fruits;
  9     
 10     public int getMoney(){    //narrow interface
 11         return money;
 12     }
 13     
 14     Memento(int money){    //wide interface
 15         this.money = money;
 16         this.fruits = new ArrayList<String>();
 17     }
 18     
 19     void addFruit(String fruit){    //wide interface
 20         fruits.add(fruit);
 21     }
 22     
 23     @SuppressWarnings("unchecked")
 24     List<String> getFruits(){    //wide interface
 25         return (List<String>)fruits.clone();
 26     }
 27 }

Memento 클래스는 Gamer의 상태를 표현하는 클래스이다.
Money와 Fruits 필드가 private가 아닌 이유는 동일한 패키지내의 Gamer 클래스로부터 필드에 자유롭게 액세스할 수 있도록 만들기 위해서다. Memento 클래스의 생성자에는 public이 없다. 따라서 Memento 클래스의 인스턴스는 누구나 만들 수 있는 것이 아니고 동일한 패키지에 속해 있는 클래스에서만 사용할 수 있다.


- Gamer.java
  1 package Memento_pattern_game;
  2
  3 import java.util.ArrayList;
  4 import java.util.Iterator;
  5 import java.util.List;
  6 import java.util.Random;
  7
  8 public class Gamer {
  9     private int money;
 10     private List<String> fruits = new ArrayList<String>();
 11     private Random random = new Random();
 12     private static String[] fruitsname = {
 13         "사과""포도""바나나""귤"
 14     };
 15     
 16     public Gamer(int money){
 17         this.money = money;
 18     }
 19     
 20     public int getMoney(){
 21         return money;
 22     }
 23     
 24     public void bet(){
 25         int dice = random.nextInt(6) + 1;
 26         if(dice == 1){
 27             money += 100;
 28             System.out.println("소지금이 증가했습니다.");
 29         }else if(dice == 2){
 30             money /= 2;
 31             System.out.println("소지금이 절반이 되었습니다.");
 32         }else if(dice == 6){
 33             String f = getFruit();
 34             System.out.println("과일(" + f + ")을 받았습니다.");
 35             fruits.add(f);
 36         }else{
 37             System.out.println("변한 것이 없습니다.");
 38         }
 39     }
 40     
 41     public Memento createMemento(){
 42         Memento m = new Memento(money);
 43         Iterator it = fruits.iterator();
 44         
 45         while(it.hasNext()){
 46             String f = (String)it.next();
 47             if(f.startsWith("맛있는")){
 48                 m.addFruit(f);
 49             }
 50         }
 51         
 52         return m;
 53     }
 54     
 55     public void restoreMemento(Memento memento){
 56         this.money = memento.money;
 57         this.fruits = memento.getFruits();
 58     }
 59     
 60     public String toString(){
 61         return "[money = " + money + ", fruits = " + fruits + "]";
 62     }
 63     
 64     private String getFruit(){
 65         String prefix = "";
 66         if(random.nextBoolean()){
 67             prefix = "맛있는 ";
 68         }
 69         
 70         return prefix + fruitsname[random.nextInt(fruitsname.length)];
 71     }
 72 }

Gamer 클래스는 게임을 실행하는 주인공을 표현하는 클래스이다.
createMemento는 현재의 상태를 저장하는 메소드이다. createMemento메소드에서는 Memento의 작성을 수행한다. 여기에서는 현 시점에서의 소지금과 과일을 기초로 Memento의 인스턴스를 한 개 만들고 있다. 이렇게 만들어진 Memento 인스턴스는 '현재의 Gamer 인스턴스의 상태'를 표현하고 있다.


- Main.java
  1 package Memento_pattern_Anonymous;
  2
  3 import Memento_pattern_game.Gamer;
  4 import Memento_pattern_game.Memento;
  5
  6 public class Main {
  7     public static void main(String[] args){
  8         Gamer gamer = new Gamer(100);
  9         Memento memento = gamer.createMemento();
 10         
 11         for(int i = 0; i < 100; i++){
 12             System.out.println("==== " + i);
 13             System.out.println("현상 : " + gamer);
 14             
 15             gamer.bet();
 16             
 17             System.out.println("소지금은 " + gamer.getMoney() + "원이 되었습니다.");
 18             
 19             if(gamer.getMoney() > memento.getMoney()){
 20                 System.out.println(" (많이 증가했으므로 현재의 상태를 저장하자)");
 21                 memento = gamer.createMemento();
 22             }else if(gamer.getMoney() < memento.getMoney() / 2){
 23                 System.out.println(" (많이 감소했으므로 이전의 상태로 복원하자)");
 24                 gamer.restoreMemento(memento);
 25             }
 26             
 27             try{
 28                 Thread.sleep(1000);
 29             }catch(InterruptedException e){
 30                 e.printStackTrace();
 31             }
 32             
 33             System.out.println("");
 34         }
 35     }
 36 }


- 결과



 추가로 연습문제를 풀어보도록 하자.
문제. 직렬화(serialization)의 기능을 사용하면 작성한 Memento의 인스턴스를 파일로 저장할 수 있다. 다음과 같은 기능을 만족하도록 예제 프로그램을 변경해라.
(1) 파일 game.dat가 존재하지 않을 때에는 소지금 100원부터 시작한다.
(2) 소지금이 많이 증가하면 Memento의 인스턴스를 파일 game.dat로 저장한다.
(3) 프로그램 실행 시에 파일 game.dat가 존재하면 그 파일에 저장되어 있는 상태에서 시작한다.

직렬화의 기능을 실행하기 위한 정보는 다음을 참고한다.
(a) 저장할 Memento 클래스는 java.io.Serializable인스턴스를 구현한다.
(b) 저장할 경우 ObjectOutputStream 클래스의 writeObject 메소드를 이용한다.
(c) 복원할 경우 ObjectInputStream 클래스의 readObject 메소드를 이용한다.

수정된 부분만 보도록 하겠다.

- Memento.java
  1 package Memento_pattern_game;
  2
  3 import java.io.Serializable;
  4 import java.util.ArrayList;
  5 import java.util.List;
  6
  7 public class Memento implements Serializable{
  8     int money;
  9     ArrayList<String> fruits;
 10     
 11     public int getMoney(){    //narrow interface
 12         return money;
 13     }
 14     
 15     Memento(int money){    //wide interface
 16         this.money = money;
 17         this.fruits = new ArrayList<String>();
 18     }
 19     
 20     void addFruit(String fruit){    //wide interface
 21         fruits.add(fruit);
 22     }
 23     
 24     @SuppressWarnings("unchecked")
 25     List<String> getFruits(){    //wide interface
 26         return (List<String>)fruits.clone();
 27     }
 28 }
 29
 
Serializable만 implements 해주면 된다.
참고로 Java Serializatoin은 자바 객체를 저장하거나 전송하기 위하여 자바 객체의 코드를 다시 복원가능한 형태의 Stream으로 직렬화 시켜주는 것을 말한다.


- Main.java
  1 package Memento_pattern_Anonymous;
  2
  3 import java.io.FileInputStream;
  4 import java.io.FileNotFoundException;
  5 import java.io.FileOutputStream;
  6 import java.io.IOException;
  7 import java.io.ObjectInput;
  8 import java.io.ObjectInputStream;
  9 import java.io.ObjectOutput;
 10 import java.io.ObjectOutputStream;
 11 import java.util.zip.DeflaterOutputStream;
 12 import java.util.zip.InflaterInputStream;
 13
 14 import Memento_pattern_game.Gamer;
 15 import Memento_pattern_game.Memento;
 16
 17 public class Main {
 18     public static final String FILENAME = "game.dat";
 19     public static void main(String[] args) {
 20         Gamer gamer = new Gamer(100);
 21         Memento memento = loadMemento();
 22         
 23         if(memento != null){
 24             System.out.println("지난번 저장한 결과에서 시작합니다.");
 25             gamer.restoreMemento(memento);
 26         }else{
 27             System.out.print("새로 시작합니다.");
 28             memento = gamer.createMemento();
 29         }
 30         
 31         for(int i = 0; i < 100; i++){
 32             System.out.println("==== " + i);
 33             System.out.println("현상 : " + gamer);
 34             
 35             gamer.bet();
 36             
 37             System.out.println("소지금은 " + gamer.getMoney() + "원이 되었습니다.");
 38             
 39             if(gamer.getMoney() > memento.getMoney()){
 40                 System.out.println(" (많이 증가했으므로 현재의 상태를 저장하자)");
 41                 memento = gamer.createMemento();
 42                 saveMemento(memento);
 43             }else if(gamer.getMoney() < memento.getMoney() / 2){
 44                 System.out.println(" (많이 감소했으므로 이전의 상태로 복원하자)");
 45                 gamer.restoreMemento(memento);
 46             }
 47             
 48             try{
 49                 Thread.sleep(1000);
 50             }catch(InterruptedException e){
 51                 e.printStackTrace();
 52             }
 53             
 54             System.out.println("");
 55         }
 56     }
 57     
 58     public static void saveMemento(Memento memento){
 59         try{
 60             ObjectOutput oop = new ObjectOutputStream(new DeflaterOutputStream(new FileOutputStream(FILENAME)));
 61             //ObjectOutput oop = new ObjectOutputStream(new FileOutputStream(FILENAME));
 62             oop.writeObject(memento);
 63             oop.close();
 64         }catch(IOException e){
 65             e.printStackTrace();
 66         }
 67     }
 68     
 69     public static Memento loadMemento(){
 70         Memento memento = null;
 71         try{
 72             //ObjectInput oi = new ObjectInputStream(new FileInputStream(FILENAME));
 73             ObjectInput oi = new ObjectInputStream(new InflaterInputStream(new FileInputStream(FILENAME)));
 74             memento = (Memento)oi.readObject();
 75             oi.close();
 76         }catch(FileNotFoundException e){
 77             System.out.println(e.toString());
 78         }catch(IOException e){
 79             e.printStackTrace();
 80         }catch(ClassNotFoundException e){
 81             e.printStackTrace();
 82         }
 83         
 84         return memento;
 85     }
 86 }

처음 내가 예제를 풀때는 저장하는 부분과 로드하는 부분을 Originator인 Gamer.java에다가 구현을 했다. 하지만 잘못된 답............ 정답을 보니 Caretaker인 Main에다가 구현을 해놨더라. 책을 읽었는데도 이런 실수를 ㅜㅜ.. 확실히 역할을 알고 역할에 맞는 부분에다가 코딩을 해야 코드 확장성이 좋더라.. 된장.
61, 72번째 주석 처리된 부분은 압출을 '하느냐 않하느냐'이다. 확실히 큰 데이터일 경우에는 압축을 하는게 용량을 줄일 수 있을 것이다.
그리고 60, 73번째 줄에서는 ObjectXXXXStream형 변수를 사용하지 않고, 인터페이스인 ObjectXXX를 사용하고 있다는거에 주목할 필요가 있다.


[출처] Java 언어로 배우는 디자인 패턴 입문

'프로그래밍 > Design Pattern' 카테고리의 다른 글

Template Method Pattern  (0) 2012.01.30
State Pattern  (0) 2012.01.27
Memento Pattern  (0) 2012.01.26
Observer Pattern  (0) 2012.01.25
Adapter Pattern  (0) 2012.01.24
Iterator Pattern  (0) 2012.01.23
TAG