apple/DesignPattern & Architecture

[Swift] Memento Pattern

lgvv 2022. 4. 13. 19:26

Memento Pattern

 

✅ Memento Pattern

 

아래의 문서를 구입하여 영어 문서를 번역하고 이해한 것을 바탕으로 글을 작성하고 있습니다.

https://www.raywenderlich.com/books/design-patterns-by-tutorials/v3.0/chapters/7-memento-pattern

 

Design Patterns by Tutorials, Chapter 7: Memento Pattern

The memento pattern allows an object to be saved and restored. you can use this pattern to implement a save game system, where the originator is the game state (such as level, health, number of lives, etc), the memento is saved data, and the caretaker is t

www.raywenderlich.com

 

 

 

✅ Memento Pattern

 

memento pattern을 사용하면 객체를 저장하고 복원할 수 있다.

memento pattern

 

1. originator는 저장하거나 복원하는 객체이다.

2. memento는 저장된 상태를 보여준다.

3. caretaker는 originator에게 요청하고, 응답으로 memento를 받는다. caretaker는 memento를 유지하고, 나중에 originator에게 memento를 전달하여 originator의 상태를 복원할 책임이 있다.

 

엄격하게 요구되는 사항은 아니지만, iOS 앱에서는 일반적으로 Encoder를 통해서 originator의 상태를 memento로 인코드하고, Decoder를 통해서 memento를 originator로 디코드 합니다. 

이를 통해서 인코딩과 디코딩 로직을 originators를 통해서 재사용 할 수 있습니다.

에를 들어, JSONEncoder와 JSONDecoder가 각각 JSON 데이터로부터 각기 인코딩 및 디코딩 되도록 허락해줍니다.

 

✅ When should you use it?

memento 패턴은 너가 객체의 상태를 저장하고 나중에 복원하기 원할때 언제든지 사용해라.

 

예를들어, 이 패턴을 이용하여 게임 시스템의 저장을 구현할 수 있다. originator는 게임의 상태(레벨, 건강, 생명의 갯수 등) memento는 저장된 데이터, caretaker는 게임의 시스템입니다.

 

이전 상태의 스택을 나타내는 memetos의 배열을 유지할 수도 있습니다. IDEs or 그래픽 소프트웨어에서 undo/redo 같은 기능을 구현할 수 있습니다. (실행 및 취소 기능 구현 가능)

 

✅ Playground example

이 예제는 간단한 게임시스템에 대한 것을 만들어 봅니다.

우선 첫번째로 originator를 만듭니다.

import Foundation

// MARK: - 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
  }
}

위의 코드가 originator에 대한 코드 부분입니다. Game이라는 클래스 내부에 State가 있고, 내부에 작업을 처리(handle)하는 메소드들이 위치해 있습니다. 그리고 너는 Game과 State를 Codable을 준수합니다.

 

[여기서 잠깐!! Codable이란 무엇일까?]

 - 애플이 Swift 4에서 소개했습니다. Codable을 준수하는 모든 유형은 외부에서 내부로 혹은 내부에서 외부로 변환될 수 있는 유형입니다. 필수적으로 스스로 저장하고 복원할 수 있는 유형입니다. 저장하고 복원할 수 있다는 것은 다소 친숙하게 들릴 수 있는데, 이번 memento에서 원하는 작업입니다. 

 

Game과 State에서 사용하는 모든 프로퍼티는 이미 Codable을 준수하므로 컴파일러는 필요한 모든 Codable 프로토콜 메소드를 자동으로 생성합니다. String, Int, Double 및 기타 대부분의 Swift 제공 유형은 기본적으로 Codable을 따릅니다. 

 

더 공식적으로 Codable은 Encodable과 Decodable 프로토콜을 결합한 typealias입니다. 따라서 아래와 같이 정의되어 있습니다.

typealias Codable = Decodable & Encodable

인코딩이 가능한 유형은 Encoder에 의해 외부 표현으로 변환될 수 있다. 외부 표현의 실제 유형은 사용하는 구체적인 인코더에 따라 다릅니다. 다행히 Foundation은 객체를 JSON 데이터로 변환하기 위한 JSONEncoder를 포함하여 몇 가지 기본 인코더를 제공하고 있습니다!! 

 

좋아요!! 이제 이론을 이해했습니다. 

 

다음으로는 memento가 필요합니다. 아래의 코드를 추가하세요!!

// MARK: - Memento
typealias GameMemento = Data

기술적으로, 위의 코드를 선언할 필요가 전혀 없습니다. 오히려 위의 코드는 GameMemeto의 실제 데이터라고 알려줍니다. 이것은 Encoder에 의해 생성되고 복원 시 디코더에서 사용됩니다.

 

다음으로는 caretaker가 필요합니다. 아래의 코드를 추가하세요!!

// MARK: - CareTaker
public class GameSystem {

  // 1
  private let decoder = JSONDecoder()
  private let encoder = JSONEncoder()
  private let userDefaults = UserDefaults.standard

  // 2
  public func save(_ game: Game, title: String) throws {
    let data = try encoder.encode(game)
    userDefaults.set(data, forKey: title)
  }

  // 3
  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
  }
}

위의 코드가 수행하는 작업은 아래와 같습니다.

1. 먼저 디코더, 인코더 및 userDefaults에 대한 속성을 선언합니다. 디코더를 사용하여 데이터에서 Game을 디코딩 하고, 인코더를 사용하여 userDefaults를 사용하여 데이터를 디스크에 유지합니다. 앱을 다시 실행하더라도 저장된 게임 데이터는 계속 사용할 수 있습니다.

2. save를 통해서 저장 로직을 먼저 캡슐화합니다. 먼저 인코더를 사용하여 전달된 게임을 인코딩 합니다. 이 작업은 오류를 발생할 수도 있어서 try를 붙여야 하빈다. 그런 다음 userDefaults 내의 주어진 제목 아래에 결과 데이터를 저장합니다!

3. load도 마찬가지로 로직을 먼저 캡슐화하빈다. 먼저 주어진 title에 대해서 userDefaults에서 data 데이터를 가져옵니다. 그런 다음 디코더를 이용하여 디코딩 합니다. 두 작업 중 하나가 실패하면 error.gameNotFound에 대한 사용자 지정 오류를 발생합니다. 두 작업이 모두 성공하면 return Game을 반환합니다.

 

이제부터는 클래스를 사용할 것입니다!

// MARK: - Example
var game = Game()
game.monstersEatPlayer()
game.rackUpMassivePoints()

여기에서 게임을 시뮬레이션 하합니다. 플레이어는 괴물에게 잡아먹히지만, 그녀는 다시 돌아와서 엄청난 점수를 얻습니다.

 

다음으로 플레이 그라운드 제일 아래에 아래의 코드를 추가합니다.

// Save Game
let gameSystem = GameSystem()
try gameSystem.save(game, title: "Best Game Ever")

여기에서 플레이어가 게임을 의기양양하게 저장하는 것을 시뮬레이션합니다. 아마도 그 직후 친구들에게 자랑할 것입니다. 

물론 그녀는 자신의 게임을 깨려고 노력할 것이므로 새로운 Game을 시작할 것입니다. 플레이 그라운드 아래에 코드를 추가합니다.

 

// New Game
game = Game()
print("New Game Score: \(game.state.score)")

여기에서 새 Game의 인스턴스를 만들고 game.state.score 프린트 해봅니다.

New Game Score: 0

위와 같은 결과가 프린트되면서 기본값이 설정되어 있음을 증명합니다.

 

플레이어는 이전 게임을 로드할 수 있습니다.

// Load Game
game = try! gameSystem.load(title: "Best Game Ever")
print("Loaded Game Score: \(game.state.score)")

플레이어는 이전 Game을 load할 수도 있습니다. 그리고 점수를 print합니다.

 

Loaded Game Score: 9002

 

✅ What should you be careful about?

Codable 프로퍼티를 추가하거나 삭제할 때 주의하세요. 인코딩과 디코딩 둘다 throw를 이용하여 error를 던질 수 있습니다. 만약 try!를 사용하여 강제 언래핑을 하는 경우 필요한 데이터가 없으면 앱이 크래시(다운) 됩니다.

 

이 문제를 완화하려면 작업이 성공할 것이라는 확신이 없는 한 try!를 사용하지 마세요. 모델을 변경할 경우에도 미리 계획해야합니다. 

 

예를 들어, 보델의 버전을 지정하거나 버전이 지정된 DB를 사용할 수 있습니다. 하지만 너는 버전 업그레이드를 처리(handle)하는 방법을 신중하게 고민해야 합니다. 새 버전이 나타날 때마다 이전 데이터를 삭제하거나, 이전 데이터에서 새 데이터로 변환하는 업그레이드 경로를 만들거나, 이러한 접근 방식을 조합하여 사용할 수도 있습니다.

 

 

✅ Tutorial project

이 부분은 데이터를 Strategy Pattern과 연결하여 데이터를 처리하는 부분이 생각보다 어려웠다.

UserDefault로 로컬에 데이터를 저장하는 부분인데, 이 부분을 모아서 관리한다는 특징이 있어 보인다.

내가 개발할 때 주로 UserDefaultsManager를 만들어서 propertyWrapper를 이용했는데, 이 패턴이 비슷해 보였다.

 

https://github.com/lgvv/DesignPattern/tree/main/memento-pattern/RabbleWabble

 

GitHub - lgvv/DesignPattern: ✨ 디자인 패턴을 공부합니다!

✨ 디자인 패턴을 공부합니다! Contribute to lgvv/DesignPattern development by creating an account on GitHub.

github.com

 

 

✅ Key points

이 장에서는 메멘토 패턴에 대해서 공부했다.

1. 메멘토 패턴은 저장하고 복원한다. 이 패턴은 originator, menento and caretaker

2. originator는 객체를 저장된 상태입니다. memento는 상태를 저장하고, caretaker를 handle한다.

3. iOS는 인코더와 디코더를 제공하고, 이를 통해 인코딩 및 디코딩을 originator에서 사용할 수 있다.