메멘토 패턴

정선호·2023년 5월 30일
0

Design Patterns

목록 보기
22/24

메멘토 패턴

위키피디아 - 메멘토 패턴
설명 및 스도코드

  • 객체의 구현 세부 사항을 공개하지 않으면서 해당 객체의 이전 상태를 저장하고 복원할 수 있게 해주는 행동 디자인 패턴

메멘토 패턴의 구조

중첩된 클래스들에 기반한 구현

이 패턴의 고전적인 구현은 수많은 인기 프로그래밍 언어(예: C++, C# 및 자바)에서 사용할 수 있는 중첩 클래스에 대한 지원에 의존한다

이 구현에서 메멘토 클래스는 오리지네이터 내부에 중첩된다. 이것은 오리지네이터가 메멘토의 필드들과 메서드들이 비공개로 선언된 경우에도 접근할 수 있도록 한다. 반면에, 케어테이커는 메멘토의 필드들과 메서드들에 매우 제한된 접근 권한을 가지므로 메멘토들을 스택에 저장할 수는 있지만 그들의 상태를 변조할 수는 없다.

  • 오리지네이터(Originator)
    • 자신의 상태에 대한 스냅샷들을 생성할 수 있으며, 필요시 스냅샷에서 자신의 상태를 복원할 수 있다
  • 메멘토(Memento)
    • 오리지네이터의 상태의 스냅샷 역할을 하는 값 객체
    • 관행적으로 메멘토는 불변으로 만든 후 생성자를 통해 데이터를 한 번만 전달
  • 케어테이커(Caretaker)
    • 언제, 왜 오리지네이터의 상태를 캡처해야하는지 뿐만 아니라 상태가 복원되야 하는 시기도 알고 있다
    • 메멘토들의 스택을 저장하여 오리지네이터의 기록을 추적할 수 있다
    • 오리지네이터가 과거로 돌아가야 할 때 케어테이커는 맨 위의 메멘토를 스택에서 가져온 후 오리지네이터의 목원 메소드를 전달한다

중첩 인터페이스에 기반한 구현

중첩 클래스들을 지원하지 않는 프로그래밍 언어(예: PHP)에 적합한 대안적 구현 방식이다.

중첩 클래스들이 없는 경우, 케어테이커들이 명시적으로 선언된 중개 인터페이스를 통해서만 메멘토와 작업할 수 있는 규칙을 만들어 메멘토의 필드들에 대한 접근을 제한할 수 있다. 이 인터페이스는 메멘토의 메타데이터와 관련된 메서드들만 선언한다.

반면에 오리지네이터들은 메멘토 객체와 직접 작업하여 메멘토 클래스에 선언된 필드들과 메서드들에 접근할 수 있다. 이 접근 방식의 단점은 메멘토의 모든 구성원을 공개(public)로 선언해야 한다는 것이다.

더 엄격한 캡슐화를 사용한 구현

다른 클래스들이 오리지네이터의 상태를 메멘토를 통해 접근할 가능성을 완전히 제거하고자 할 때 유용하다.

이 구현 방식을 사용하면 여러 유형의 오리지네이터들과 메멘토들을 보유할 수 있다. 각 오리지네이터는 그에 상응하는 메멘토 클래스와 함께 작동한다. 오리지네이터들과 메멘토들은 자신의 상태를 누구에게도 노출하지 않는다.

케어테이커들은 이제 메멘토들에 저장된 상태의 변경에 명시적인 제한을 받는다. 또 케어테이커 클래스는 복원 메서드가 이제 메멘토 클래스에 정의되어 있으므로 오리지네이터에게서 독립된다.

각 메멘토는 그것을 생성한 오리지네이터와 연결된다. 오리지네이터는 자신의 상태 값들과 함께 자신을 메멘토의 생성자에 전달한다. 이러한 클래스 간의 긴밀한 관계 덕분에 메멘토는, 오리지네이터가 적절한 세터들을 정의했을 경우, 자신의 오리지네이터의 상태를 복원할 수 있다.

메멘토 패턴의 적용

  • 객체의 이전 상태를 복원할 수 있도록 객체의 상태의 스냅샷들을 생성하려는 경우에 사용
    • 메멘토는 비공개 필드들을 포함하여 객체의 상태의 전체 복사본들을 만들 수 있도록 하고 이 복사본들을 객체와 별도로 저장할 수 있도록 한다.
    • 대부분의 개발자는 이 패턴을 '실행 취소'의 사용과 관련지어 기억하지만, 트랜잭션들을 처리할 때​(즉, 오류 발생 시 작업을 롤백해야 할 때)​도 필수 불가결한 패턴이다.
  • 객체의 필드들/게터들/세터들을 직접 접근하는 것이 해당 객체의 캡슐화를 위반할 때 사용
    • 객체가 스스로 자신의 상태의 스냅샷의 생성을 담당하게 한다. 다른 객체는 스냅샷을 읽을 수 없으므로 원래 객체의 상태 데이터는 안전하다.

다른 패턴과의 관계

  • '실행 취소'를 구현할 때 커맨드와 메멘토 패턴을 함께 사용할 수 있다. 그러면 커맨드들은 대상 객체에 대해 다양한 작업을 수행하는 역할을 맡는다. 반면, 메멘토들은 커맨드가 실행되기 직전에 해당 객체의 상태를 저장한다.
  • 메멘토 패턴을 반복자 패턴과 함께 사용하여 현재 순회 상태를 포착하고 필요한 경우 롤백할 수 있다.
  • 때로는 프로토타입이 메멘토 패턴의 더 간단한 대안이 될 수 있으며, 이 패턴은 상태를 기록에 저장하려는 객체가 간단하고 외부 리소스에 대한 링크가 없거나 링크들이 있어도 이들을 재설정하기 쉬운 경우에 작동한다.

메멘토 패턴의 예시

  • 오리지네이터
class Game {
    var level: Int = 0
    var score: Int = 0
    
    func setLevel(level: Int) {
        self.level = level
    }
    
    func setScore(score: Int) {
        self.score = score
    }
    
    // Memento 생성 - Memento 생성자 주입으로 Originator 자체를 전달
    func createMemento() -> SaveData {
        print("Level : \(self.level), Score: \(self.score) 상태를 저장합니다.\n")
        return SaveData(originator: self)
    }
    
    func printCurrentState() {
        print("현재 상태 Level : \(self.level), Score: \(self.score)")
    }
}
  • 메멘토
class SaveData {
    private var originator: Game
    private var level: Int = 0
    private var score: Int = 0
    
    // Memento 생성 시 Originator의 State를 전달받음 (이후 수정하지 않음)
    init(originator: Game) {
        self.originator = originator
        self.level = originator.level
        self.score = originator.score
    }
    
    // Originator의 State를 복원
    func recover() {
        self.originator.setLevel(level: self.level)
        self.originator.setScore(score: self.score)
    }
}
  • 케어테이커
class GameDataSystem {
    private var history: [SaveData] = []
    
    func save(memento: SaveData) {  // Memento 스냅샷들을 저장
        self.history.append(memento)
    }
    
    func recoverLastState() {  // 가장 최근의 스냅샷을 복원
        if let mementoSnapshot: SaveData = self.history.popLast() {
            print("최근 저장 상태를 불러옵니다.\n")
            mementoSnapshot.recover()
        } else {
            print("저장 기록이 없습니다.\n")
        }
    }
}
  • 클라이언트
let originator = Game()
let caretaker = GameDataSystem()
originator.setLevel(level: 5)
originator.setScore(score: 3)

// Originator가 Memento 스냅샷을 생성
let memento = originator.createMemento()
// Memento 스냅샷을 CareTaker에 저장
caretaker.save(memento: memento) // 출력 - Level : 5, Score: 3 상태를 저장합니다.

// 게임을 하다가 레벨과 점수가 이전 저장 했을 때 보다 낮아짐
originator.setLevel(level: 1)
originator.setScore(score: 1)
originator.printCurrentState() // 출력 - 현재 상태 Level : 1, Score: 1

// 레벨과 점수 복구
caretaker.recoverLastState() // 출력 - 최근 저장 상태를 불러옵니다.
originator.printCurrentState() // 출력 - 현재 상태 Level : 5, Score: 3

caretaker.recoverLastState() // 출력 - 저장 기록이 없습니다.
profile
학습한 내용을 빠르게 다시 찾기 위한 저장소

0개의 댓글