728x90
Memento Pattern
메멘토 패턴은 Object를 저장하거나 불러오게 할 수 있다. 3가지 부분으로 나눠진다.
- Originator: 저장되어야 할 객체이다.
- memento: 저장된 상태를 나타낸다.
- CareTaker:
- save: careTaker는 originator에게 저장을 요청하고 memento 로부터 응답을 받는다.
- restore: memento를 유지하는 역할을 담당하며 추후에 originator state 를 복원하기 위해 originator 에게 memento 를 제공한다.
iOS Menento Pattern
- iOS 앱은 전형적으로 Encoder 을 사용하여 originator의 state를 메멘토로 인코딩(save) 하고 Decoder를 사용하여 메멘토를 originator 로 다시 디코딩(restore) 한다.
- 이것은 인코딩과 디코딩 로직이 다양한 Originators에 대해 재사용 되게 허용해 준다.
- 예를들어 JSONEncoder 과 JSONDecoder는 객체를 JSON 데이터로 encode 나 decode 할 수 있다.
언제 사용해야 할까?
- 객체의 상태를 저장했다가 나중에 복원하고 싶을때
- 게임 저장 시스템에 사용할 수 있다. originator 는 gate state(level, health, number of lives)가 될 수 있고 memento는 저장된 데이터, careTaker는 게임 시스템으로 볼 수 있다.
- memento들을 배열로 유지시켜 이전 상태들을 Stack 처럼 표현할 수 있다. 이를 통해 IDE 나 그래픽 소프트웨어에서 undo/redo 기능을 구현할 수 있다.
Memento Pattern - Behavior 패턴이다.
save 과 restoration 행위에 대한 패턴이기 때문에 Behavior 패턴에 속한다.
Memento Pattern - 예제 코드
- 게임 프로퍼티를 들고있는 내부 State를 가지고 있는 Game 을 정의한다.
- Game 은 게임 내의 활동들에 대한 메소드를 들고 있다.
- Game 과 State를 Codable에 순응하게 선언해야 한다.
import Foundation
// Originator
public class Game: Codable {
public class State: Codable {
public var attemptsRemaining: Int = 3
public var level: Int = 1
public var score: Int = 0
}
public var state = State()
public func rackUpMassivePoints() {
state.score += 9002
}
public func monstersEatPlayer() {
state.attemptsRemaining -= 1
}
}
왜 Codable 에 순응하게 해야될까?
- 애플은 Codable 을 Swift4 에 소개했다.
- Codable 에 순응하는 타입은 애플이 말하자면 "convert itself into and out of an external representation" 이라고 한다.
- 결국 자기 자신을 save 하고 restore 할 수 있는 타입을 뜻한다.
- 바로 originator의 역할과 동일하다.
Game 과 State 가 Codable 에 순응하기 때문에 컴파일러는 자동으로 필요한 모든 Codable 프로토콜 메소드를 만들어 준다.
String, Int, Double 과 같은 Swift 가 제공하는 타입은 모두 Codable에 순응할 수 있다.
공식적으로 Codable 은 Encodable 과 Decodable 프로토콜을 합친 typealias 이다.
typealias Codable = Decodable & Encodable
Encodable 타입의 경우 Encoder 에 의해 외부 표현으로 변환될 수 있다.
외부 표현의 실제 타입은 사용하는 Encoder 의 종류에 따라 달라진다.
Foundation은 몇가지 기본 encoder 를 제공해 준다.
예를 들어 객체를 JSON 데이터로 변환해 주는 JSONEncoder 가 있다.
다시 예제 코드로 돌아가서
[originator]
import Foundation
// Originator
public class Game: Codable {
public class State: Codable {
public var attemptsRemaining: Int = 3
public var level: Int = 1
public var score: Int = 0
}
public var state = State()
public func rackUpMassivePoints() {
state.score += 9002
}
public func monstersEatPlayer() {
state.attemptsRemaining -= 1
}
}
[Memento]
typealias GameMemento = Data
[careTaker]
public class GameSystem {
private let decoder = JSONDecoder()
private let encoder = JSONEncoder()
private let userDefaults = UserDefaults.standard
public func save(_ game: Game, title: String) throws {
let data = try encoder.encode(game)
userDefaults.set(data,forKey:title)
}
public 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
}
public enum Error: String, Swift.Error {
case gameNotFound
}
}
- decoder 과 encoder, userDefaults 에 대한 프로퍼티를 선언한다.
- decoder는 Games 를 Data 로 부터 decode 하고
encoder 는 Games 를 Data 로 encode 하고
userDefaults 는 Data 를 디스크에 저장한다. - save(_:title:) 는 저장 로직을 캡슐화 시킨다. 먼저 encoder 을 통해 game을 저장한다. 이 작업은 에러를 발생 시킬 수 있기 때문에 try 로 예외처리를 해준다. 다음에 결과 데이터를 주어진 title 에 맞게 userDefaults 에 저장한다.
- load(title:) 도 마찬가지로 불러오는 로직을 캡슐화 한다. userDefaults 로 부터 데이터를 가지고 와서 decoder 를 통해 Data를 Game 으로 decode 한다. 만약 둘중 하나라도 작업이 실패할 경우 Error.gameNotFound 가 던져진다. 둘 모두 성공할 경우 결과값인 저장된 Game 을 받을 수 있다.
Memento Pattern 을 이용해 Game 상태를 저장하기 위한 준비는 끝났고
실제로 저장과 불러오는 코드를 작성해 보자
//게임이 진행되고 플레이어는 한번 먹히고 한번 포인트를 왕창 받는다.
var game = Game()
game.monstersEatPlayer()
game.rackUpMAssivePoints()
//게임 저장
let gameSystem = GameSystem()
try gameSystem.save(game, title: "Best Game Ever")
//새로운 게임
game = Game()
print("New Game Score: \(game.state.score)")
//New Game Score: 0
//게임 불러오기
game = try! gameSystem.load(title: "Best Game Ever")
print("Loaded Game Score: \(game.state.score)")
//Loaded Game Score: 9002
Memento Pattern 사용시 주의할 점
- Codable 프로퍼티를 추가하거나 삭제할때 주의해야 한다.
-> Encoding 과 Decoding 시 error 를 throw 할 수 있다. try! 처럼 강제로 unwrapping 하면 필요한 데이터가 없을 수 도 있고 그렇게 되면 앱이 뻗는다. - 위 문제를 완화하기 위해서는 try! 사용을 지양하고 모델을 바꿀때 먼저 계획을 잘 짜야한다.
- 예를들어 모델들을 버젼관리 할 수 있다, 하지만 버젼 업그레이드의 경우 어떻게 다룰지 잘 생각해야한다.
1) 이전 새로운 버전이 나올 때 마다 이전 데이터를 삭제한다.
2) 새로운 업그레이드 path 를 만들어 old 데이터를 new 데이터로 바꾼다.
3) 1,2번 방법을 조합하여 사용한다.
728x90
'iOS > Design Pattern' 카테고리의 다른 글
Design Pattern - Builder Pattern (0) | 2020.08.09 |
---|---|
Design Pattern - Observer Pattern (0) | 2020.08.09 |
Design Pattern - Singleton Pattern (0) | 2020.08.09 |
Design Pattern - Strategy Pattern (0) | 2020.08.07 |
Design Pattern - Delegation Pattern (0) | 2020.08.06 |
댓글