[DesignPattern] Memento 패턴
개요
기본적으로 객체의 저장과 복원에 대한 패턴이다.
위 다이어그램에서 나타나듯 3가지로 구성되어 있다.
- Originator : 현재 state 값을 저장할 대상이 되는 객체이자 복원이 될 객체. Memento의 생성, 접근은 Originator로만 가능해야함
- Memento : 특정 시점에 저장된 Originator의 state 값
- CareTaker : Originator의 state가 언제 저장 되어야할 지, 복원이 되어야할지 알고 있음 Originator의 저장과 복원에 대한 관리를 함, Memento를 관리하는 책임이 있음
구조 및 흐름
CareTaker는 저장과 복구를 하기위해 Originator를 참조한다
- Originator는 저장 메세지를 받으면 현재 시점의 state를 기반으로 Memento 객체를 생성하여 반환
- 복원 메세지를 받으면 전달된 Memento의 state를 기반으로 Originator의 state 값을 복원함
예제 코드
어떤 게임이 있다고 가정해보자.
해당 게임은 시도할 수 있는 기회가 정해져있고 현재의 점수, 그리고 난이도를 가지고 있다
특정 시점에 게임을 저장하면 점수, 남은 기회, 난이도가 저장되고
추후에 불러오면 현재 게임의 점수, 남은 기회, 난이도는 불러온 데이터를 기반으로 갱신되야한다.
이를 기반으로 코드를 작성해보자
// MARK: - Originator
class Game: Codable {
class State: Codable {
var attemptsRemaining: Int = 3
var level: Int = 1
var score: Int = 0
}
var state = State()
func rackUpMassivePoints() {
state.score += 9002
}
func monsterEatPlayers() {
state.attemptsRemaining -= 1
}
}
게임은 상태를 가지기 때문에 Originator가 될 것이다.
언제 저장되고, 다시 복원시킬지를 관리하는 CareTaker도 작성해보자
// MARK: - CareTaker
class GameSystem {
// 디코더, 인코더, userDefault 선언
private let decoder = JSONDecoder()
private let encoder = JSONEncoder()
private let userDefaults = UserDefaults.standard
func save(_ game: Game, title: String) throws {
let data = try encoder.encode(game)
userDefaults.set(data, forKey: title)
}
func load(title: String) throws -> Game {
guard let data = userDefaults.data(forKey: title),
let game = try? decoder.decode(Game.self, from: data)
else{
throw Error.gameNotFound
}
return game
}
enum Error: String, Swift.Error {
case gameNotFound
}
}
save 메소드가 호출되는 시점에 Originator 역할의 game을 참조하여 현재 상태를 Memento로 생성하고
userDefault에 저장되어진다.
load(title:) 메소드가 호출되는 시점에 userDefault로부터 가져온 Memento를 반환한다.
정리하면 save메소드는 Memento를 넘겨주고, load메소드는 Memento를 반환한다
var game = Game()
game.monsterEatPlayers()
game.rackUpMassivePoints()
let gameSystem = GameSystem()
try gameSystem.save(game, title: "Best Game Ever")
game = Game()
print("new Score : \(game.state.score)")
game = try! gameSystem.load(title: "Best Game Ever")
print("Loaded Score : \(game.state.score)")
save가 호출되었을 때 game의 상태는
score : 9002
level : 1
attemptsRemaing: 2
이를 CareTaker인 gameSystem에서 save 메소드를 호출하여 Originator인 game을 전달하면
위 메소드에 의해 현재 game의 상태값을 가진 Memento를 userDefault에 저장하게 된다.
개인적으로 느끼기에는 이 예제에서 Originator와 Memento가 Game이라는 같은 타입을 사용하고 있어서
혼돈스러웠다.
많은 생각 끝에 인스턴스 game은 Originator
CareTaker gameSystem의 메소드 save에서 let data = try encoder.encode(game)
에서 data가 Memento라고 생각하면 이해가 잘 되었다.
마찬 가지로 load 메소드에서는
let game = try? decoder.decode(Game.self, from: data)
에서 game을 Memento라고 생각하면 쉬웠다.
개요에서 말한 구조, 흐름과 딱 맞아 떨어지지는 않지만 내가 생각하기에 주요 골자는
Originator는 상태값을 저장해야 하는 객체
Memento는 특정 시점에서의 Originator의 상태 값
CareTaker는 Originator를 참조하여 저장, 복원등의 로직을 수행하고 Memento를 관리하는 객체로 유연하게 생각하면 될 것 같다.
비슷하게 위의 예시를 아래와 같이 변경도 가능한 것 같다
/ MARK: - Originator
class Game {
struct State {
var score: Int = 0
var level: Int = 1
}
var state = State() {
didSet {
print("game state change \(state)")
}
}
func saveToMemento() -> State {
let result = State(score: state.score, level: state.level)
print("saved Memento \(result)")
return result
}
func restoreFromMemento(state: State) {
print("restore from Memento")
self.state = state
}
}
// MARK: - CareTaker
class GameSystem {
var mementoList = [Game.State]()
func save(memento: Game.State) {
mementoList.append(memento)
}
func restoreState(game: Game){
guard mementoList.count > 0 else { return }
game.restoreFromMemento(state: mementoList.removeLast())
}
}
let game = Game()
let gameSystem = GameSystem()
game.state.score = 20
game.state.level = 2
// 현재 시점 값 저장
gameSystem.save(memento: game.saveToMemento())
// 값 변경
game.state = Game.State()
// 복구
gameSystem.restoreState(game: game)
결과
game state change State(score: 20, level: 1)
game state change State(score: 20, level: 2)
saved Memento State(score: 20, level: 2)
game state change State(score: 0, level: 1)
restore from Memento
game state change State(score: 20, level: 2)
참고
https://velog.io/@kipsong/Memento-Pattern
댓글남기기