출처 : 디자인 패턴에 뛰어들기 - 알렉산더 슈베츠 도서
메멘토 패턴(Memento Patten)
객체의 구현 세부 사항을 공개하지 않으면서 해당 객체의 이전 상태를 저장하고 복원할 수 있게 해주는 행동 디자인패턴이다.
문제
텍스트 편집기 앱을 만들고 있다고 가정해본다.
- 당신의 편집기는 간단한 텍스트 편집 외에도 텍스트의 서식 지정, 인라인 이미지들의 삽입 등을 할 수 있다.
- 어느 날 당신은 사용자들이 텍스트에 수행된 모든 작업을 실행 취소할 수 있도록 하기로 했다.
- 이 실행 취소 기능은 수년에 걸쳐 매우 보편화되었기 때문에 오늘날의 사용자들은 모든 앱에 이 기능이 있을 것이라고 가정해본다.
- 이 기능을 구현하기 위해 직접 접근 방식을 적용하기로 했다.
- 앱은 모든 작업을 수행하기 전에 모든 객체의 상태를 기록해 어떤 스토리지에 저장한다.
- 나중에 사용자가 작업을 실행 취소하기로 하면 앱은 기록에서 가장 최신 스냅샷을 가져와 모든 객체의 상태를 복원하는 데 사용한다.
즉, 취소하기로 하면 앱은 기록에서 가장 최신 스냅샷을 가져와 모든 객체의 상태를 복원하는 데 사용한다.
앱은 작업을 실행하기 전에 객체들의 상태의 스냅샷을 저장하며, 이 스냅샷은 나중에 객체들을 이전 상태로 복원하는 데 사용할 수 있는 예시이다.
상태 스냅샷들에 대해 생각해본다.
- 상태 스냅샷은 정확히 어떻게 생성되는지 알아보면 아마도 객체의 모든 필드를 살펴본 후 해당 값들을 스토리지에 복사해야 할 것이다.
- 그러나 이는 객체의 내용에 대한 액세스 제한이 상당히 완화되어 있는 경우에만 작동할 것이다.
- 불행히도, 대부분의 실제 객체들은 모든 중요한 데이터를 비공개 필드에 숨긴다.
다른 생각으로, 객체들이 히피족처럼 열린 관계들을 선호해 그들의 상태를 공개했다고 가정해본다.
- 이렇게 가정하면 일단 위의 문제는 해결되어 원하는 대로 객체들의 상태에 대한 스냅샷들을 생성할 수 있다.
- 하지만 여전히 몇 가지 심각한 문제들이 남아 있습니다. 앞으로 당신이 일부 필드를 추가 또는 제거하거나, 편집기 클래스들을 리팩토링하기로 결정할지도 모르기 때문이다.
- 말은 쉬워 보이지만, 그렇게 하려면 영향받은 객체들의 상태를 복사하는 역할을 맡은 클래스들을 변경해야 한다.
객체의 비공개 상태는 어떻게 복사해야하는지에 대한 예시이다.
그뿐만이 아닙니다.
편집기 상태의 실제 '스냅샷'들에 어떤 데이터가 포함되어 있는지 살펴본다.
- 이 안에는 최소한 실제 텍스트, 커서 좌표, 현재 스크롤 위치 등이 포함되어 있을 것이다.
- 스냅샷을 만들려면 이러한 값들을 수집한 후 일종의 컨테이너에 넣어야 한다.
아마도 당신은 이러한 컨테이너 객체들을 기록에 해당하는 어떤 리스트에 많이 저장하게 될 것이다.
- 따라서 이 컨테이너들은 결국 한 클래스의 객체들이 될 것이다.
- 이 클래스에는 메서드는 거의 없을 테지만, 편집기의 상태를 미러링하는 필드는 많이 있을 것이다.
- 다른 객체들이 스냅샷에서 데이터를 읽고 스냅샷에 데이터를 쓸 수 있도록 하려면, 아마도 해당 스냅샷의 필드를 공개해야 할 것이다.
- 그러면 편집기의 모든 (비공개 포함) 상태들이 노출될 것이고, 이제 다른 클래스들은 스냅샷 클래스에 발생하는 모든 자그마한 변경에도 영향을 받게 될 것이다.
- 편집기의 모든 상태가 노출되지 않았다면 이러한 변경들은 외부 클래스에는 영향을 미치지 않은 채 비공개 필드와 메서드 안에서 변경이 발생하는 것으로 끝났을 것이다.
따라서, 이제 교착 상태에 빠진 것 같다.
- 클래스 내부의 세부 정보를 모두 공개하면 클래스가 너무 취약해진다.
- 하지만 클래스의 상태에 접근하지 못하게 하면 스냅샷을 생성할 수 없게 된다.
- 그러면 '실행 취소'는 대체 어떻게 구현해야 하는지 생각해 보아야 한다.
해결책
우리가 방금 경험한 모든 문제는 캡슐화의 실패로 인해 발생한다.
- 일부 객체들은 원래 해야 할 일보다 더 많은 일들을 수행하려고 한다.
- 예를 들어 이러한 객체들은 어떤 작업을 수행하는 데 필요한 데이터를 수집하기 위해 다른 객체들이 실제 작업을 수행하도록 놔두는 대신 그들의 비공개 공간을 침범한다.
- 메멘토는 상태 스냅샷들의 생성을 해당 상태의 실제 소유자인 originator(오리지네이터) 객체에 위임한다. * 그러면 다른 객체들이 '외부'에서 편집기의 상태를 복사하려 시도하는 대신, 자신의 상태에 대해 완전한 접근 권한을 갖는 편집기 클래스가 자체적으로 스냅샷을 생성할 수 있다.
- 이 패턴은 메멘토라는 특수 객체에 객체 상태의 복사본을 저장하라고 제안한다.
- 메멘토의 내용에는 메멘토를 생성한 객체를 제외한 다른 어떤 객체도 접근할 수 없다.
- 다른 객체들은 메멘토들과 제한된 인터페이스를 사용해 통신해야 한다.
- 이러한 인터페이스는 스냅샷의 메타데이터(생성 시간, 수행한 작업의 이름, 등)를 가져올 수 있도록 할 수 있지만, 스냅샷에 포함된 원래 객체의 상태는 가져오지 못한다.
오리지네이터는 메멘토에 대한 전체 접근 권한이 있지만 케어테이커(caretaker)는 메타데이터에만 접근할 수 있는 예시이다.
이러한 제한 정책을 사용하면 일반적으로 케어테이커라고 하는 다른 객체들 안에 메멘토들을 저장할 수 있다.
- 케어테이커는 제한된 인터페이스를 통해서만 메멘토와 작업하기 때문에 메멘토 내부에 저장된 상태를 변경할 수 없다.
- 동시에 오리지네이터는 메멘토 내부의 모든 필드에 접근할 수 있으므로 언제든지 자신의 이전 상태를 복원할 수 있다.
위의 텍스트 편집기 예시의 경우, 별도의 기록 클래스를 만들어 케어테이커의 역할을 하도록 할 수 있다.
- 케어테이커 내부의 메멘토들의 스택은 편집기가 작업을 실행하려고 할 때마다 계속 늘어날 것이다.
- 또 앱의 UI 내에서 이 스택을 렌더링하여 이전에 수행한 작업들의 기록을 사용자에게 표시할 수도 있다.
사용자가 실행 취소를 작동시키면 기록은 스택에서 가장 최근의 메멘토를 가져온 후 편집기에 다시 전달하여 롤백을 요청한다.
- 편집기는 메멘토에 대한 완전한 접근 권한이 있으므로 메멘토에서 가져온 값들로 자신의 상태를 변경한다.
구조
중첩된 클래스들에 기반한 구현
이 패턴의 고전적인 구현은 수많은 인기 프로그래밍 언어(예: C++, C# 및 자바)에서 사용할 수 있는 중첩 클래스에 대한 지원에 의존한다.
1.오리지네이터 클래스는 자신의 상태에 대한 스냅샷들을 생성할 수 있다
- 필요시 스냅샷에서 자신의 상태를 복원할 수도 있다.**
- 필요시 스냅샷에서 자신의 상태를 복원할 수도 있다.**
2.메멘토는 오리지네이터의 상태의 스냅샷 역할을 하는 값 객체이다.
- 관행적으로 메멘토는 불변으로 만든 후 생성자를 통해 데이터를 한 번만 전달한다.
- 관행적으로 메멘토는 불변으로 만든 후 생성자를 통해 데이터를 한 번만 전달한다.
3.케어테이커는 '언제' 그리고 '왜' 오리지네이터의 상태를 캡처해야 하는지 뿐만 아니라 상태가 복원돼야 하는 시기도 알고 있다.
- 케어테이커는 메멘토들의 스택을 저장하여 오리지네이터의 기록을 추적할 수 있다.
- 오리지네이터가 과거로 돌아가야 할 때 케어테이커는 맨 위의 메멘토를 스택에서 가져온 후 오리지네이터의 복원 메서드에 전달한다.
4.이 구현에서 메멘토 클래스는 오리지네이터 내부에 중첩된다.
- 이것은 오리지네이터가 메멘토의 필드들과 메서드들이 비공개로 선언된 경우에도 접근할 수 있도록 한다.
- 반면에, 케어테이커는 메멘토의 필드들과 메서드들에 매우 제한된 접근 권한을 가지므로 메멘토들을 스택에 저장할 수는 있지만 그들의 상태를 변조할 수는 없다.
중간 인터페이스에 기반한 구현
중첩 클래스들을 지원하지 않는 프로그래밍 언어(예: PHP)에 적합한 대안적 구현 방식이 있다.
1.중첩 클래스들이 없는 경우
- 케어테이커들이 명시적으로 선언된 중개 인터페이스를 통해서만 메멘토와 작업할 수 있는 규칙을 만들어 메멘토의 필드들에 대한 접근을 제한할 수 있다.
- 이 인터페이스는 메멘토의 메타데이터와 관련된 메서드들만 선언한다.
2.반면에 오리지네이터들은 메멘토 객체와 직접 작업하여 메멘토 클래스에 선언된 필드들과 메서드들에 접근할 수 있다.
- 이 접근 방식의 단점은 메멘토의 모든 구성원을 공개(public)로 선언해야 한다는 것이다.
더 엄격한 캡슐화를 사용한 구현
또 다른 구현이 있는데, 이 구현은 당신이 다른 클래스들이 오리지네이터의 상태를 메멘토를 통해 접근할 가능성을 완전히 제거하고자 할 때 유용하다.
1.이 구현 방식을 사용하면 여러 유형의 오리지네이터들과 메멘토들을 보유할 수 있다.
- 각 오리지네이터는 그에 상응하는 메멘토 클래스와 함께 작동합니다. 오리지네이터들과 메멘토들은 자신의 상태를 누구에게도 노출하지 않는다.
- 각 오리지네이터는 그에 상응하는 메멘토 클래스와 함께 작동합니다. 오리지네이터들과 메멘토들은 자신의 상태를 누구에게도 노출하지 않는다.
2.케어테이커들은 이제 메멘토들에 저장된 상태의 변경에 명시적인 제한을 받는다.
- 또 케어테이커 클래스는 복원 메서드가 이제 메멘토 클래스에 정의되어 있으므로 오리지네이터에게서 독립된다.
- 또 케어테이커 클래스는 복원 메서드가 이제 메멘토 클래스에 정의되어 있으므로 오리지네이터에게서 독립된다.
3.각 메멘토는 그것을 생성한 오리지네이터와 연결된다.
- 오리지네이터는 자신의 상태 값들과 함께 자신을 메멘토의 생성자에 전달한다.
- 이러한 클래스 간의 긴밀한 관계 덕분에 메멘토는, 오리지네이터가 적절한 세터들을 정의했을 경우, 자신의 오리지네이터의 상태를 복원할 수 있다.
의사코드
이 예시에서는 메멘토를 커맨드 패턴과 함께 사용하여 복잡한 텍스트 편집기의 상태의 스냅샷들을 저장하고 필요할 때 스냅샷들로부터 이전 상태를 복원할 수 있도록 힌다.
텍스트 편집기 상태에 대한 스냅샷 저장 예시이다.
커맨드 객체들은 케어테이커 역할을 한다.
- 이 객체들은 커맨드들과 관련된 작업들을 실행하기 전에 편집기의 메멘토를 가져온다.
- 사용자가 가장 최근 커맨드를 실행 취소하려고 하면 편집기는 해당 커맨드에 저장된 메멘토를 사용하여 자신을 이전 상태로 되돌릴 수 있다.
메멘토 클래스는 공개된 필드들, 게터(getter)들 또는 세터(setter)들을 선언하지 않는다.
- 따라서 어떤 객체도 자신의 내용을 변경할 수 없다.
- 메멘토들은 자신을 만든 편집기 객체에 연결된다.
- 이것은 메멘토가 데이터를 연결된 편집기 객체의 세터들을 통해 전달하여 해당 편집기의 상태를 복원할 수 있도록 한다.
- 메멘토들은 특정 편집자 객체들에 연결되어 있으므로 당신은 당신의 앱이 중앙 집중식 실행 취소 스택을 사용하여 여러 독립 편집기 창을 지원하도록 할 수 있다.
// 오리지네이터는 시간이 지남에 따라 변경될 수 있는 어떤 중요한 데이터를 보유합니다.
// 또한 자신의 상태를 메멘토 내부에 저장하는 메서드와 해당 상태를
// 메멘토로부터 복원하는 또 다른 메서드를 정의합니다.
class Editor is
private field text, curX, curY, selectionWidth
method setText(text) is
this.text = text
method setCursor(x, y) is
this.curX = x
this.curY = y
method setSelectionWidth(width) is
this.selectionWidth = width
// 현재 상태를 메멘토 내부에 저장합니다.
method createSnapshot():Snapshot is
// 메멘토는 불변 객체입니다. 이 때문에 오리지네이터는
// 자신의 상태를 메멘토의 생성자 매개변수들에 전달합니다.
return new Snapshot(this, text, curX, curY, selectionWidth)
// 메멘토 클래스는 편집기의 이전 상태를 저장합니다.
class Snapshot is
private field editor: Editor
private field text, curX, curY, selectionWidth
constructor Snapshot(editor, text, curX, curY, selectionWidth) is
this.editor = editor
this.text = text
this.curX = x
this.curY = y
this.selectionWidth = selectionWidth
// 어느 시점에 메멘토 객체를 사용하여 편집기의 이전 상태를 복원할 수 있습니다.
method restore() is
editor.setText(text)
editor.setCursor(curX, curY)
editor.setSelectionWidth(selectionWidth)
// 커맨드 객체는 케어테이커 역할을 할 수 있습니다.
// 그러면 커맨드는 오리지네이터의 상태를 변경하기 직전에 메멘토를 얻습니다.
// 실행 취소가 요청되면 커맨드는 메멘토에서 오리지네이터의 상태를 복원합니다.
class Command is
private field backup: Snapshot
method makeBackup() is
backup = editor.createSnapshot()
method undo() is
if (backup != null)
backup.restore()
// …
적용
메멘토는 객체의 이전 상태를 복원할 수 있도록 객체의 상태의 스냅샷들을 생성하려는 경우에 사용하세요.
- 메멘토는 비공개 필드들을 포함하여 객체의 상태의 전체 복사본들을 만들 수 있도록 하고 이 복사본들을 객체와 별도로 저장할 수 있도록 한다.
- 대부분의 개발자는 이 패턴을 '실행 취소'의 사용과 관련지어 기억하지만, 트랜잭션들을 처리할 때(즉, 오류 발생 시 작업을 롤백해야 할 때)도 필수 불가결한 패턴이다.
이 패턴은 또 객체의 필드들/게터들/세터들을 직접 접근하는 것이 해당 객체의 캡슐화를 위반할 때 사용하세요.
- 메멘토는 객체가 스스로 자신의 상태의 스냅샷의 생성을 담당하게 한다.
- 다른 객체는 스냅샷을 읽을 수 없으므로 원래 객체의 상태 데이터는 안전하다.
구현방법
1.어떤 클래스가 오리지네이터의 역할을 할 것인지 결정하세요.
- 프로그램이 이 유형의 중심 객체를 사용하는지 아니면 여러 개의 작은 객체들을 사용하는지 아는 것이 중요하다.
- 프로그램이 이 유형의 중심 객체를 사용하는지 아니면 여러 개의 작은 객체들을 사용하는지 아는 것이 중요하다.
2.메멘토 클래스를 만드세요.
- 하나씩 오리지네이터 클래스 내부에 선언된 필드들을 미러링하는 필드들의 집합을 선언해야 한다.
- 하나씩 오리지네이터 클래스 내부에 선언된 필드들을 미러링하는 필드들의 집합을 선언해야 한다.
3.메멘토 클래스를 변경할 수 없도록 하세요.
- 메멘토는 생성자를 통해 데이터를 한 번만 받아야 하며, 그 클래스에는 세터들이 없어야 한다.
- 메멘토는 생성자를 통해 데이터를 한 번만 받아야 하며, 그 클래스에는 세터들이 없어야 한다.
4.사용하고 있는 프로그래밍 언어가 중첩 클래스를 지원하면 오리지네이터 내부에 메멘토를 중첩하세요.
- 그렇지 않은 경우, 메멘토 클래스에서 빈 인터페이스를 추출한 후 다른 모든 객체가 메멘토를 참조하는 데 사용하도록 해야한다.
- 인터페이스에 일부 메타데이터 작업을 추가할 수 있지만 오리지네이터의 상태를 노출해서는 안된다.
5.오리지네이터 클래스에 메멘토들을 생성하는 메서드를 추가하세요.
- 오리지네이터는 자신의 상태를 메멘토의 생성자의 하나 또는 여러 인수들을 통해 메멘토에게 전달해야 한다.
- 이 메서드의 반환 유형은 (이전 단계에서 추출했다고 가정했을 때) 이전 단계에서 추출한 인터페이스의 유형이어야 한다.
- 메멘토 생성 메서드는 메멘토 클래스와 직접 작동해야 한다.
6.오리지네이터의 클래스에 자신의 상태를 복원하는 메서드를 추가하세요.
- 이 메서드는 메멘토 객체를 인수로 받아들여야 한다.
- 이전 단계에서 인터페이스를 추출했다면 이 인터페이스의 유형을 매개변수의 유형으로 지정해야한다.
- 이 경우, 들어오는 객체를 메멘토 클래스에 타입캐스팅해야 한다.
- 왜냐하면 오리지네이터에게 이 객체에 대한 완전한 접근 권한이 필요하기 때문이다.
7.케어테이커는 커맨드 객체든, 기록이든, 아니면 완전히 다른 무언가를 나타낼 때 새로운 메멘토들을 오리지네이터로부터 언제 요청해야 하는지, 이 메멘토들을 어떻게 저장하고, 언제 특정 메멘토로부터 오리지네이터를 복원해야 하는지를 알아야 한다.
8.케어테이커들과 오리지네이터들 간의 연결은 메멘토 클래스로 이동시킬 수 있다.
- 이 경우, 각 메멘토는 자신을 생성한 오리지네이터와 연결되어야 한다.
- 복원 메서드도 메멘토 클래스로 이동할 수 있다.
- 그러나 이 모든 것은 메멘토 클래스가 오리지네이터에 중첩되거나 오리지네이터 클래스가 메멘토 클래스의 상태를 오버라이드하기에 충분한 세터들을 제공하는 경우에만 의미가 있다.
장단점
장점
- 캡슐화를 위반하지 않고 객체의 상태의 스냅샷들을 생성할 수 있다.
- 케어테이커가 오리지네이터의 상태의 기록을 유지하도록 하여 오리지네이터의 코드를 단순화할 수 있다.
단점
- 클라이언트들이 메멘토들을 너무 자주 생성하면 앱이 많은 RAM을 소모할 수 있다.
- 케어테이커들은 더 이상 쓸모없는 메멘토들을 파괴할 수 있도록 오리지네이터의 수명주기를 추적해야 한다.
- PHP, 파이썬 및 JavaScript와 같은 대부분의 동적 프로그래밍 언어에서는 메멘토 내의 상태가 그대로 유지된다고 보장할 수 없다.
다른 패턴과의 관계
당신은 '실행 취소'를 구현할 때 커맨드와 메멘토 패턴을 함께 사용할 수 있다.
- 그러면 커맨드들은 대상 객체에 대해 다양한 작업을 수행하는 역할을 맡는다.
- 반면, 메멘토들은 커맨드가 실행되기 직전에 해당 객체의 상태를 저장한다.
메멘토 패턴을 반복자 패턴과 함께 사용하여 현재 순회 상태를 포착하고 필요한 경우 롤백할 수 있다.
때로는 프로토타입이 메멘토 패턴의 더 간단한 대안이 될 수 있다.
- 이 패턴은 상태를 기록에 저장하려는 객체가 간단하고 외부 리소스에 대한 링크가 없거나 링크들이 있어도 이들을 재설정하기 쉬운 경우에 작동한다.
'Self-Dev > Design Patterns R&D' 카테고리의 다른 글
[31] 디자인 패턴 목록 - 행동 디자인 패턴 - 상태 패턴(State Patten) (0) | 2024.07.22 |
---|---|
[30] 디자인 패턴 목록 - 행동 디자인 패턴 - 옵서버 패턴(Observer Patten) (0) | 2024.07.21 |
[28] 디자인 패턴 목록 - 행동 디자인 패턴 - 중재자 패턴(Mediator Patten) (0) | 2024.07.14 |
[27] 디자인 패턴 목록 - 행동 디자인 패턴 - 반복자 패턴(Iterator Patten) (0) | 2024.07.13 |
[26] 디자인 패턴 목록 - 행동 디자인 패턴 - 커멘드 패턴(Command Patten) (0) | 2024.07.12 |