메멘토(Memento) 패턴은 어떠한 객체 내부의 상태를 외부에 저장해놓고 저장된 정보를 복원하고 싶을때 사용되는 패턴입니다. 이 패턴의 가장 큰 특징은 어떤 객체의 상태를 밖에 저장을 하려면 일반적으로 객체의 상태가 온전히 다 노출이 되야합니다. 그런데 캡슐화를 유지한 상태에서 객체 내부의 상태를 외부에 저장하고 그것을 복원할 수 있게 할 수 있습니다.
위 그림의 구조를 살펴보면 CareTaker, Originator, Memento로 이루어 지는데 여기서 가장 중요한건 Originator입니다. Originator의 역할은 본래의 상태를 저장하고 복원하고싶은 원래의 데이터를 가지고 있습니다. CareTaker는 Originator의 내부정보를 CareTaker가 가지고와서 저장하고 있고 가지고 있는 정보를 복원할 수 있는 일종의 외부 클래스입니다. 그리고 Originator의 내부정보를 Memento라는 곳으로 추상화하는 것입니다. CareTaker가 Originator의 상세한 정보를 가지고 있는게 아니라 Originator의 내부정보를 Memento라는 타입으로 가지고 있습니다. 그래서 Originator는 2가지의 메서드를 제공해줘야하는데 createMementor메서드로 Originator의 내부정보를 Memento로 추상화해서 리턴해주는 역할을 합니다. 그리고 외부에서 전달받은 Memento정보를 가지고 자신의 정보를 복원할 수 있는 restore메서드를 제공해야합니다. Memento같은 경우는 불변객체로 내부상태 정보가 한번 세팅이되면 변경이 불가능하도록 해야합니다.
메멘토 패턴이 필요한 예제 코드를 살펴보겠습니다. Game클래스에 redTeamScore, blueTeamScore 2가지 필드가 있습니다.
public class Game implements Serializable {
private int redTeamScore;
private int blueTeamScore;
// getter, setter
}
만약 redTeamScore, blueTeamScore의 값들을 저장 했다가 나중에 게임을 재진행 할 때 이 값을 가진상태로 진행해야할 때 단순하게 아래와 같이 redTeamScore가 10이고 blueTeamScore가 20인 각각의 스코어를 밖에 getter를 이용해 변수에 저장하고 다시 restoredGame에 변수값으로 기존에 Game이 가진 데이터를 복원시킬 수 있습니다. 하지만 이렇게되면 캡슐화가 깨진상태라고 볼 수 있습니다. 왜냐하면 Client가 Game에 있는 내부정보가 뭐가 있는지 일일이 알고 있어야 하기 때문입니다. 현재 Client는 Game의 redTeamScore, blueTeamScore를 알고 있어야 합니다. 만약 Game의 내부정보가 변경되면 Client의 코드변경이 일어납니다. 결국은 이런 의존성을 끊어줘야 Client코드가 변경되지 않게 됩니다. 이런경우 메멘토 패턴을 적용해 볼 수 있습니다.
public class Client {
public static void main(String[] args) {
Game game = new Game();
game.setRedTeamScore(10);
game.setBlueTeamScore(20);
int blueTeamScore = game.getBlueTeamScore();
int redTeamScore = game.getRedTeamScore();
Game restoredGame = new Game();
restoredGame.setBlueTeamScore(blueTeamScore);
restoredGame.setRedTeamScore(redTeamScore);
}
}
메멘토 패턴을 적용해서 기존코드를 수정해 보겠습니다. 아래와 같이 기존 코드는 Client가 Game의 상태를 상세하게 알고 있습니다. 메멘토 패턴을 적용하면 Game의 상태를 알지 않더라도 내부정보를 저장했다가 복원하는 형태로 Client와 Game의 결합도를 낮출 수 있습니다.
public class Client {
public static void main(String[] args) {
Game game = new Game();
game.setRedTeamScore(10);
game.setBlueTeamScore(20);
int blueTeamScore = game.getBlueTeamScore();
int redTeamScore = game.getRedTeamScore();
Game restoredGame = new Game();
restoredGame.setBlueTeamScore(blueTeamScore);
restoredGame.setRedTeamScore(redTeamScore);
}
}
Game상태를 저장하도록 Memento역할을 하는 GameSave를 정의하겠습니다. 불변이어야 하기 때문에 blueTeamScore, redTeamScore필드를 final로 선언합니다.
public final class GameSave {
private final int blueTeamScore;
private final int redTeamScore;
public GameSave(int blueTeamScore, int redTeamScore) {
this.blueTeamScore = blueTeamScore;
this.redTeamScore = redTeamScore;
}
public int getBlueTeamScore() {
return blueTeamScore;
}
public int getRedTeamScore() {
return redTeamScore;
}
}
Game같은경우는 Memento를 사용해서 저장할수있는 방법과 복원할 수 있는 방법을 save, restore메서드로 제공합니다.
public class Game {
private int redTeamScore;
private int blueTeamScore;
// getter, setter
public GameSave save() {
return new GameSave(this.blueTeamScore, this.redTeamScore);
}
public void restore(GameSave gameSave) {
this.blueTeamScore = gameSave.getBlueTeamScore();
this.redTeamScore = gameSave.getRedTeamScore();
}
}
Client입장에서 Game의 정보를 save메서드로 저장했다가 중간의 Game의 상태 값이 변해도 restore메서드로 복원해서 저장된 값을 출력하는것을 알 수 있습니다.
public class Client {
public static void main(String[] args) {
Game game = new Game();
game.setBlueTeamScore(10);
game.setRedTeamScore(20);
GameSave save = game.save();
game.setBlueTeamScore(12);
game.setRedTeamScore(22);
game.restore(save);
System.out.println(game.getBlueTeamScore()); // 10
System.out.println(game.getRedTeamScore()); // 20
}
}
메멘토(memento) 패턴을 사용하면 캡슐화를 지키면서 상태 객체 상태 스냅샷을 만들 수 있습니다. 객체 상태를 저장하고 복원하는 역할을 CareTaker에게 위임 할 수 있고 객체 상태가 바뀌어도 클라이언트코드는 변경되지 않습니다. 하지만 많은 정보를 저장하는 Memento를 자주 생성하는 경우 메모리 사용량에 많은 영향을 줄 수 있습니다.