[Design pattern] 3-5. 메멘토 패턴 (Memento pattern)


Study/Design Pattern  2021. 11. 16. 20:00

안녕하세요. 명월입니다.


이 글은 디자인 패턴의 메멘토 패턴(Memento pattern)에 대한 글입니다.


메멘토 패턴은 클래스의 현재 상태를 다른 클래스로 저장하는 패턴입니다.

클래스의 데이터를 저장하는 형태로는 꼭 메멘토 패턴이 아닌 클래스 복사(인스턴스 복사)로 현재의 상태를 보관할 수 있습니다. 그러나 그렇게 되면 현재의 인스턴스가 아닌 새로운 인스턴스로 객체를 생성하는 것이고, 만약에 객체 안에 리소스(IO나 Socket)를 사용하고 있다면 새로운 커넥션을 생성해야 하는 문제점도 발생하는 것입니다.

즉, 메멘토 패턴은 인스턴스의 객체는 변하지 않으면서 안의 값만 저장하여, 상태를 복구하는 역할을 하는 패턴이 메멘토 패턴입니다.

출처 - https://en.wikipedia.org/wiki/Memento_pattern
#pragma once
#define _CRT_SECURE_NO_DEPRECATE
#include <stdio.h>
#include <iostream>
using namespace std;
// 메멘토 클래스
class Memento {
private:
  // Node 클래스에서 사용되는 변수
  int data;
  int state;
public:
  // 생성자
  Memento(int data, int state) {
    this->data = data;
    this->state = state;
  }
  // data 값 취득
  int getData() {
    return this->data;
  }
  // state 값 취득
  int getState() {
    return this->state;
  }
};
// Node 클래스
class Node {
private:
  // 맴버 변수
  int data;
  int state;
public:
  // data 맴버 변수 설정 함수
  void setData(int data) {
    this->data = data;
  }
  // state 맴버 변수 설정 함수
  void setState(int state) {
    this->state = state;
  }
  // 메멘토 클래스로 맴버 변수 설정
  void setMemento(Memento* memento) {
    this->data = memento->getData();
    this->state = memento->getState();
  }
  // 현재 상태를 메멘토 인스턴스로 리턴
  Memento* getMemento() {
    return new Memento(this->data, this->state);
  }
  // 출력 함수
  void print() {
    // 콘솔 출력
    cout << "data - " << this->data << "  state - " << this->state << endl;
  }
};
// memento 데이터를 파일로 쓰기 함수
void writeMemento(Memento* memento) {
  // 파일 리소스 포인터를 취득, 파라미터 w는 작성이다. (리소스 취득)
  FILE* fp = fopen("d:\\work\\memento.dat", "w");
  // 포인터가 null이면 프로그램을 종료한다.
  if (fp == NULL) {
    cout << "File open failed" << endl;
    return;
  }
  // 데이터를 파일에 쓰기
  fwrite(memento, 1, sizeof(Memento), fp);
  // 파일을 닫는다. (리소스 반환)
  fclose(fp);
}
// memento 데이터를 파일로부터 읽기 함수
Memento* readMemento() {
  // 인스턴스 생성
  Memento* memento = (Memento*)malloc(sizeof(Memento));
  // 파일 리소스 포인터를 취득, 파라미터 r는 읽기이다. (리소스 취득)
  FILE* fp = fopen("d:\\work\\memento.dat", "r");
  // 포인터가 null이면 프로그램을 종료한다.
  if (fp == NULL) {
    cout << "File open failed" << endl;
    return nullptr;
  }
  // 데이터 읽기
  fread(memento, 1, sizeof(Memento), fp);
  // 파일을 닫는다. (리소스 반환)
  fclose(fp);
  // 리턴
  return memento;
}
// 실행 함수
int main()
{
  // Node 인스턴스 생성
  Node node;
  // 데이터 설정
  node.setData(10);
  node.setState(1);
  // 콘솔 출력
  node.print();
  // 메멘토 인스턴스 취득
  Memento* memento = node.getMemento();
  // 파일 저장
  writeMemento(memento);
  // 메모리 해제
  delete memento;
  // 데이터 재설정
  node.setData(11);
  node.setState(2);
  // 콘솔 출력
  node.print();
  // 파일로부터 메멘토 데이터 취득
  memento = readMemento();
  // Node 인스턴스에 데이터 재설정
  node.setMemento(memento);
  // 콘솔 출력
  node.print();
  // 메모리 해제
  delete memento;

  return 0;
}

위 예제에서 Node 클래스의 상태를 저장하는 Memento 인스턴스를 취득했습니다.

이 Memento 클래스에서 해당 클래스를 파일로 저장하고, 또 파일로 읽어와서 다시 Node 인스턴스에 설정하면 데이터가 이전 데이터로 복원되는 것을 확인할 수 있습니다.

예를 들면, 게임에서 중간 세이브하고 다시 파일로 읽어 들여서 현재 상태를 복원하는 것과 같은 형태의 패턴입니다.

import java.io.*;

// Node 클래스
class Node implements Serializable {
  // 직렬화 ID
  private static final long serialVersionUID = 1L;

  // Memento 클래스
  class Memento implements Serializable {
    // 직렬화 ID
    private static final long serialVersionUID = 1L;
    // Node 클래스로부터 보관할 맴버 변수
    private int data;
    private int state;
  }

  // 맴버 변수
  private int data;
  private int state;

  // data의 set 함수
  public void setData(int data) {
    this.data = data;
  }

  // state의 set 함수
  public void setState(int state) {
    this.state = state;
  }

  // 메멘토 인스턴스 취득 함수
  public Serializable getMenent() {
    // 인스턴스 생성
    Memento memento = new Memento();
    // 보관할 데이터 설정
    memento.data = data;
    memento.state = state;
    // 리턴
    return memento;
  }

  // 메멘토 인스턴스로 상태 복원
  public void setMement(Serializable memento) {
    // 클래스가 Memento 클래스 타입이 아니면 처리 중단
    if (memento.getClass() != Memento.class) {
      // 콘솔 출력
      System.out.println("The class type does not match.");
      return;
    }
    // 형 변환
    Memento memento1 = (Memento) memento;
    // Node 클래스에 데이터 재설정
    this.data = memento1.data;
    this.state = memento1.state;
  }

  // 출력 함수
  public void print() {
    // 콘솔 출력
    System.out.println("data - " + this.data + " state - " + this.state);
  }
}

public class Program {
  // 메멘토 인스턴스를 파일로 저장할 함수
  private static void writeFile(Serializable serialize) {
    // 파일 설정
    File file = new File("d:/work/memento.dat");
    // 파일이 존재하면 삭제
    if (file.exists()) {
      // 삭제
      file.delete();
    }
    // 직렬화
    try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
      try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
        oos.writeObject(serialize);
        // 메멘토 인스턴스를 byte 배열로 변환했다.
        byte[] data = baos.toByteArray();
        // 파일 스트림을 이용해 변환한 data를 파일로 저장한다.
        try (FileOutputStream stream = new FileOutputStream(file)) {
          // 파일 쓰기
          stream.write(data, 0, data.length);
        }
      }
    } catch (Throwable e) {
      // 예외 처리
      throw new RuntimeException(e);
    }
  }
  // 메멘토 인스턴스를 파일로부터 읽어오는 함수
  private static Serializable readFile() {
    // 파일 설정
    File file = new File("d:/work/memento.dat");
    // 파일이 존재하지 않으면
    if (!file.exists()) {
      // 에러 처리
      throw new RuntimeException(new FileNotFoundException());
    }
    // 역직렬화
    try (FileInputStream stream = new FileInputStream(file)) {
      byte[] data = new byte[(int) file.length()];
      // 파일로부터 바이너리를 읽어 드린다.
      stream.read(data, 0, data.length);
      try (ByteArrayInputStream bais = new ByteArrayInputStream(data)) {
        try (ObjectInputStream ois = new ObjectInputStream(bais)) {
          // byte 배열의 데이터를 메멘토 인스턴스로 변환한다.
          return (Serializable) ois.readObject();
        }
      }
    } catch (Throwable e) {
      // 예외 처리
      throw new RuntimeException(e);
    }
  }
  // 실행 함수
  public static void main(String[] args) {
    // Node 인스턴스 생성
    Node node = new Node();
    // 맴버 변수 설정
    node.setData(10);
    node.setState(1);
    // 콘솔 출력
    node.print();
    // 메멘토 인스턴스을 생성
    var memento = node.getMenent();
    // 파일 저장
    writeFile(memento);
    // 맴버 변수 재설정
    node.setData(11);
    node.setState(2);
    // 콘솔 출력
    node.print();
    // 파일로부터 메멘토 인스턴스 생성
    memento = readFile();
    // 인스턴스 값 재설정
    node.setMement(memento);
    // 콘솔 출력
    node.print();
  }
}

C/C++로 짠 소스의 구조와 거의 비슷합니다. 단지 Memeno 클래스를 Node 클래스의 인라인으로 만들었습니다.

즉, Memento 클래스는 사양으로는 상태를 저장하는 역할을 가지고 있기 때문에 Node 클래스의 이외에서는 데이터 설정을 할 수 없도록 하는 것이 기본 원칙입니다.

상태를 마음대로 설정 가능하게 되다면, 그건 메멘토 패턴이 아니고 단순 파라미터를 주고 받는 클래스의 역할이 되는 것입니다.

using System;
using System.Reflection;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

// Node 클래스
class Node
{
  // Reflection을 위한 타입 설정
  private Type reflectionMemento = typeof(Memento);
  // 맴버 변수
  public int Data { get; set; }
  public int State { get; set; }
  // 메멘토 클래스 (파일로 직렬화하기 위한 어트리뷰트 설정)
  [Serializable]
  class Memento
  {
    // 맴버 변수
    private int data;
    private int state;
  }
  // 출력 함수
  public void Print()
  {
    // 콘솔 출력
    Console.WriteLine("Data - " + Data + " State - " + State);
  }
  // 메멘토 인스턴스 취득 함수
  public object GetMemento()
  {
    // 인스턴스 생성
    Memento memento = new Memento();
    // 맴버 변수가 private로 설정되어 있기 때문에 접근하기 위해서는 Reflection을 이용한다. 변수 설정
    reflectionMemento.GetField("data", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(memento, this.Data);
    reflectionMemento.GetField("state", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(memento, this.State);
    // 인스턴스 리턴
    return memento;
  }
  // 메멘토 인스턴스로 멤버 변수 설정 함수
  public void SetMemento(Object obj)
  {
    // 파라미터가 Memento 클래스가 아니면 예외 처리
    if (obj.GetType() != reflectionMemento)
    {
      // 예외 처리
      throw new Exception("The class type does not match.");
    }
    // 맴버 변수가 private로 설정되어 있기 때문에 접근하기 위해서는 Reflection을 이용한다. 값을 취득
    this.Data = (int)reflectionMemento.GetField("data", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(obj);
    this.State = (int)reflectionMemento.GetField("state", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(obj);
  }
}
class Program
{
  // 메멘토 인스턴스를 파일로 저장하는 함수
  static void writeMemento(object memento)
  {
    // 직렬화 클래스
    var formatter = new BinaryFormatter();
    // 직렬화 데이터를 파일로 저장한다.
    using (FileStream stream = new FileStream("d:\\work\\memento.dat", FileMode.Create, FileAccess.Write))
    {
      // 직렬화
      formatter.Serialize(stream, memento);
    }
  }
  // 메멘토 인스턴스를 파일로 읽어오는 함수
  static object readMemento()
  {
    // 직렬화 클래스
    var formatter = new BinaryFormatter();
    // 파일로부터 직렬화 데이터를 읽어온다.
    using (FileStream stream = new FileStream("d:\\work\\memento.dat", FileMode.Open, FileAccess.Read))
    {
      // 역직렬화
      return formatter.Deserialize(stream);
    }
  }
  // 실행 함수
  static void Main(string[] args)
  {
    // 인스턴스 생성
    Node node = new Node();
    // 맴버 변수 설정
    node.Data = 10;
    node.State = 1;
    // 출력 함수
    node.Print();
    // 메멘토 인스턴스 취득
    object memento = node.GetMemento();
    // 메멘토 인스턴스를 파일로 출력
    writeMemento(memento);
    // Node 인스턴스의 맴버 변수 설정
    node.Data = 11;
    node.State = 2;
    // 출력 함수
    node.Print();
    // 메멘토 인스턴스를 파일로 읽어 온다.
    memento = readMemento();
    // Node 인스턴스에 메멘토 재설정
    node.SetMemento(memento);
    // 출력 함수
    node.Print();
    // 아무 키나 누르시면 종료합니다.
    Console.WriteLine("Press any key...");
    Console.ReadKey();
  }
}

C#도 Java와 비슷한 소스입니다. Memento 클래스가 Node 클래스의 인라인으로 설정하여 Node 클래스 외부에서는 설정하지 못하게 작성했습니다.

그러나 C#은 인라인 클래스라도 public이 아니면 접근을 하지 못하기 때문에 Reflection을 이용하여 직접 private 변수를 설정할 수 있게 작성했습니다.


여기까지 디자인 패턴의 메멘토 패턴(Memento pattern)에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.