메멘토 패턴
의도
메멘토는 객체의 구현 세부 사항을 공개하지 않으면서 해당 객체의 이전 상태를 저장하고 복원할 수 있게 해주는 행동 디자인 패턴입니다.
문제
텍스트 편집기 앱을 만들고 있다고 상상해보세요. 당신의 편집기는 간단한 텍스트 편집 외에도 텍스트의 서식 지정, 인라인 이미지들의 삽입 등을 할 수 있습니다.
어느 날 당신은 사용자들이 텍스트에 수행된 모든 작업을 실행 취소할 수 있도록 하기로 했습니다. 이 실행 취소 기능은 수년에 걸쳐 매우 보편화되었기 때문에 오늘날의 사용자들은 모든 앱에 이 기능이 있을 것이라고 가정합니다. 이 기능을 구현하기 위해 직접 접근 방식을 적용하기로 했습니다. 앱은 모든 작업을 수행하기 전에 모든 객체의 상태를 기록해 어떤 스토리지에 저장합니다. 나중에 사용자가 작업을 실행 취소하기로 하면 앱은 기록에서 가장 최신 스냅샷을 가져와 모든 객체의 상태를 복원하는 데 사용합니다.
상태 스냅샷들에 대해 생각해 봅시다. 상태 스냅샷은 정확히 어떻게 생성될까요? 아마도 객체의 모든 필드를 살펴본 후 해당 값들을 스토리지에 복사해야 할 것입니다. 그러나 이는 객체의 내용에 대한 액세스 제한이 상당히 완화되어 있는 경우에만 작동할 것입니다. 불행히도, 대부분의 실제 객체들은 모든 중요한 데이터를 비공개 필드에 숨깁니다.
이 문제는 일단 무시하고, 객체들이 히피족처럼 열린 관계들을 선호해 그들의 상태를 공개했다고 가정해 봅시다. 이렇게 가정하면 일단 위의 문제는 해결되어 원하는 대로 객체들의 상태에 대한 스냅샷들을 생성할 수 있습니다. 하지만 여전히 몇 가지 심각한 문제들이 남아 있습니다. 앞으로 당신이 일부 필드를 추가 또는 제거하거나, 편집기 클래스들을 리팩토링하기로 결정할지도 모르기 때문입니다. 말은 쉬워 보이지만, 그렇게 하려면 영향받은 객체들의 상태를 복사하는 역할을 맡은 클래스들을 변경해야 합니다.
그뿐만이 아닙니다. 편집기 상태의 실제 '스냅샷'들에 어떤 데이터가 포함되어 있는지 살펴봅시다. 이 안에는 최소한 실제 텍스트, 커서 좌표, 현재 스크롤 위치 등이 포함되어 있을 겁니다. 스냅샷을 만들려면 이러한 값들을 수집한 후 일종의 컨테이너에 넣어야 합니다.
아마도 당신은 이러한 컨테이너 객체들을 기록에 해당하는 어떤 리스트에 많이 저장하게 될 겁니다. 따라서 이 컨테이너들은 결국 한 클래스의 객체들이 될 것입니다. 이 클래스에는 메서드는 거의 없을 테지만, 편집기의 상태를 미러링하는 필드는 많이 있을 겁니다. 다른 객체들이 스냅샷에서 데이터를 읽고 스냅샷에 데이터를 쓸 수 있도록 하려면, 아마도 해당 스냅샷의 필드를 공개해야 할 것입니다. 그러면 편집기의 모든 (비공개 포함) 상태들이 노출될 것이고, 이제 다른 클래스들은 스냅샷 클래스에 발생하는 모든 자그마한 변경에도 영향을 받게 될 것입니다. 편집기의 모든 상태가 노출되지 않았다면 이러한 변경들은 외부 클래스에는 영향을 미치지 않은 채 비공개 필드와 메서드 안에서 변경이 발생하는 것으로 끝났을 겁니다.
이제 교착 상태에 빠진 것 같습니다. 클래스 내부의 세부 정보를 모두 공개하면 클래스가 너무 취약해집니다. 하지만 클래스의 상태에 접근하지 못하게 하면 스냅샷을 생성할 수 없게 됩니다. 그러면 '실행 취소'는 대체 어떻게 구현해야 할까요?
해결책
우리가 방금 경험한 모든 문제는 캡슐화의 실패로 인해 발생합니다. 일부 객체들은 원래 해야 할 일보다 더 많은 일들을 수행하려고 합니다. 예를 들어 이러한 객체들은 어떤 작업을 수행하는 데 필요한 데이터를 수집하기 위해 다른 객체들이 실제 작업을 수행하도록 놔두는 대신 그들의 비공개 공간을 침범합니다.
메멘토는 상태 스냅샷들의 생성을 해당 상태의 실제 소유자인 originator(오리지네이터) 객체에 위임합니다. 그러면 다른 객체들이 '외부'에서 편집기의 상태를 복사하려 시도하는 대신, 자신의 상태에 대해 완전한 접근 권한을 갖는 편집기 클래스가 자체적으로 스냅샷을 생성할 수 있습니다.
이 패턴은 메멘토라는 특수 객체에 객체 상태의 복사본을 저장하라고 제안합니다. 메멘토의 내용에는 메멘토를 생성한 객체를 제외한 다른 어떤 객체도 접근할 수 없습니다. 다른 객체들은 메멘토들과 제한된 인터페이스를 사용해 통신해야 합니다. 이러한 인터페이스는 스냅샷의 메타데이터(생성 시간, 수행한 작업의 이름, 등)를 가져올 수 있도록 할 수 있지만, 스냅샷에 포함된 원래 객체의 상태는 가져오지 못합니다.
이러한 제한 정책을 사용하면 일반적으로 케어테이커라고 하는 다른 객체들 안에 메멘토들을 저장할 수 있습니다. 케어테이커는 제한된 인터페이스를 통해서만 메멘토와 작업하기 때문에 메멘토 내부에 저장된 상태를 변경할 수 없습니다. 동시에 오리지네이터는 메멘토 내부의 모든 필드에 접근할 수 있으므로 언제든지 자신의 이전 상태를 복원할 수 있습니다.
위의 텍스트 편집기 예시의 경우, 별도의 기록 클래스를 만들어 케어테이커의 역할을 하도록 할 수 있습니다. 케어테이커 내부의 메멘토들의 스택은 편집기가 작업을 실행하려고 할 때마다 계속 늘어날 것입니다. 또 당신은 앱의 UI 내에서 이 스택을 렌더링하여 이전에 수행한 작업들의 기록을 사용자에게 표시할 수도 있습니다.
사용자가 실행 취소를 작동시키면 기록은 스택에서 가장 최근의 메멘토를 가져온 후 편집기에 다시 전달하여 롤백을 요청합니다. 편집기는 메멘토에 대한 완전한 접근 권한이 있으므로 메멘토에서 가져온 값들로 자신의 상태를 변경합니다.
구조
중첩된 클래스들에 기반한 구현
이 패턴의 고전적인 구현은 수많은 인기 프로그래밍 언어(예: C++, C# 및 자바)에서 사용할 수 있는 중첩 클래스에 대한 지원에 의존합니다.
-
오리지네이터 클래스는 자신의 상태에 대한 스냅샷들을 생성할 수 있으며, 필요시 스냅샷에서 자신의 상태를 복원할 수도 있습니다.
-
메멘토는 오리지네이터의 상태의 스냅샷 역할을 하는 값 객체입니다. 관행적으로 메멘토는 불변으로 만든 후 생성자를 통해 데이터를 한 번만 전달합니다.
-
케어테이커는 '언제' 그리고 '왜' 오리지네이터의 상태를 캡처해야 하는지 뿐만 아니라 상태가 복원돼야 하는 시기도 알고 있습니다.
케어테이커는 메멘토들의 스택을 저장하여 오리지네이터의 기록을 추적할 수 있습니다. 오리지네이터가 과거로 돌아가야 할 때 케어테이커는 맨 위의 메멘토를 스택에서 가져온 후 오리지네이터의 복원 메서드에 전달합니다.
-
이 구현에서 메멘토 클래스는 오리지네이터 내부에 중첩됩니다. 이것은 오리지네이터가 메멘토의 필드들과 메서드들이 비공개로 선언된 경우에도 접근할 수 있도록 합니다. 반면에, 케어테이커는 메멘토의 필드들과 메서드들에 매우 제한된 접근 권한을 가지므로 메멘토들을 스택에 저장할 수는 있지만 그들의 상태를 변조할 수는 없습니다.
중간 인터페이스에 기반한 구현
중첩 클래스들을 지원하지 않는 프로그래밍 언어(예: PHP)에 적합한 대안적 구현 방식이 있습니다.
-
중첩 클래스들이 없는 경우, 당신은 케어테이커들이 명시적으로 선언된 중개 인터페이스를 통해서만 메멘토와 작업할 수 있는 규칙을 만들어 메멘토의 필드들에 대한 접근을 제한할 수 있습니다. 이 인터페이스는 메멘토의 메타데이터와 관련된 메서드들만 선언합니다.
-
반면에 오리지네이터들은 메멘토 객체와 직접 작업하여 메멘토 클래스에 선언된 필드들과 메서드들에 접근할 수 있습니다. 이 접근 방식의 단점은 메멘토의 모든 구성원을 공개(public)로 선언해야 한다는 것입니다.
더 엄격한 캡슐화를 사용한 구현
또 다른 구현이 있는데, 이 구현은 당신이 다른 클래스들이 오리지네이터의 상태를 메멘토를 통해 접근할 가능성을 완전히 제거하고자 할 때 유용합니다.
-
이 구현 방식을 사용하면 여러 유형의 오리지네이터들과 메멘토들을 보유할 수 있습니다. 각 오리지네이터는 그에 상응하는 메멘토 클래스와 함께 작동합니다. 오리지네이터들과 메멘토들은 자신의 상태를 누구에게도 노출하지 않습니다.
-
케어테이커들은 이제 메멘토들에 저장된 상태의 변경에 명시적인 제한을 받습니다. 또 케어테이커 클래스는 복원 메서드가 이제 메멘토 클래스에 정의되어 있으므로 오리지네이터에게서 독립됩니다.
-
각 메멘토는 그것을 생성한 오리지네이터와 연결됩니다. 오리지네이터는 자신의 상태 값들과 함께 자신을 메멘토의 생성자에 전달합니다. 이러한 클래스 간의 긴밀한 관계 덕분에 메멘토는, 오리지네이터가 적절한 세터들을 정의했을 경우, 자신의 오리지네이터의 상태를 복원할 수 있습니다.
의사코드
이 예시에서는 메멘토를 커맨드 패턴과 함께 사용하여 복잡한 텍스트 편집기의 상태의 스냅샷들을 저장하고 필요할 때 스냅샷들로부터 이전 상태를 복원할 수 있도록 합니다.
커맨드 객체들은 케어테이커 역할을 합니다. 이 객체들은 커맨드들과 관련된 작업들을 실행하기 전에 편집기의 메멘토를 가져옵니다. 사용자가 가장 최근 커맨드를 실행 취소하려고 하면 편집기는 해당 커맨드에 저장된 메멘토를 사용하여 자신을 이전 상태로 되돌릴 수 있습니다.
메멘토 클래스는 공개된 필드들, 게터(getter)들 또는 세터(setter)들을 선언하지 않습니다. 따라서 어떤 객체도 자신의 내용을 변경할 수 없습니다. 메멘토들은 자신을 만든 편집기 객체에 연결됩니다. 이것은 메멘토가 데이터를 연결된 편집기 객체의 세터들을 통해 전달하여 해당 편집기의 상태를 복원할 수 있도록 합니다. 메멘토들은 특정 편집자 객체들에 연결되어 있으므로 당신은 당신의 앱이 중앙 집중식 실행 취소 스택을 사용하여 여러 독립 편집기 창을 지원하도록 할 수 있습니다.
적용
메멘토는 객체의 이전 상태를 복원할 수 있도록 객체의 상태의 스냅샷들을 생성하려는 경우에 사용하세요.
메멘토는 비공개 필드들을 포함하여 객체의 상태의 전체 복사본들을 만들 수 있도록 하고 이 복사본들을 객체와 별도로 저장할 수 있도록 합니다. 대부분의 개발자는 이 패턴을 '실행 취소'의 사용과 관련지어 기억하지만, 트랜잭션들을 처리할 때(즉, 오류 발생 시 작업을 롤백해야 할 때)도 필수 불가결한 패턴입니다.
이 패턴은 또 객체의 필드들/게터들/세터들을 직접 접근하는 것이 해당 객체의 캡슐화를 위반할 때 사용하세요.
메멘토는 객체가 스스로 자신의 상태의 스냅샷의 생성을 담당하게 합니다. 다른 객체는 스냅샷을 읽을 수 없으므로 원래 객체의 상태 데이터는 안전합니다.
구현방법
-
어떤 클래스가 오리지네이터의 역할을 할 것인지 결정하세요. 프로그램이 이 유형의 중심 객체를 사용하는지 아니면 여러 개의 작은 객체들을 사용하는지 아는 것이 중요합니다.
-
메멘토 클래스를 만드세요. 하나씩 오리지네이터 클래스 내부에 선언된 필드들을 미러링하는 필드들의 집합을 선언하세요.
-
메멘토 클래스를 변경할 수 없도록 하세요. 메멘토는 생성자를 통해 데이터를 한 번만 받아야 하며, 그 클래스에는 세터들이 없어야 합니다.
-
사용하고 있는 프로그래밍 언어가 중첩 클래스를 지원하면 오리지네이터 내부에 메멘토를 중첩하세요. 그렇지 않은 경우, 메멘토 클래스에서 빈 인터페이스를 추출한 후 다른 모든 객체가 메멘토를 참조하는 데 사용하도록 하세요. 인터페이스에 일부 메타데이터 작업을 추가할 수 있지만 오리지네이터의 상태를 노출해서는 안 됩니다.
-
오리지네이터 클래스에 메멘토들을 생성하는 메서드를 추가하세요. 오리지네이터는 자신의 상태를 메멘토의 생성자의 하나 또는 여러 인수들을 통해 메멘토에게 전달해야 합니다.
이 메서드의 반환 유형은 (이전 단계에서 추출했다고 가정했을 때) 이전 단계에서 추출한 인터페이스의 유형이어야 합니다. 메멘토 생성 메서드는 메멘토 클래스와 직접 작동해야 합니다.
-
오리지네이터의 클래스에 자신의 상태를 복원하는 메서드를 추가하세요. 이 메서드는 메멘토 객체를 인수로 받아들여야 합니다. 이전 단계에서 인터페이스를 추출했다면 이 인터페이스의 유형을 매개변수의 유형으로 지정하세요. 이 경우, 들어오는 객체를 메멘토 클래스에 타입캐스팅해야 합니다. 왜냐하면 오리지네이터에게 이 객체에 대한 완전한 접근 권한이 필요하기 때문입니다.
-
케어테이커는 커맨드 객체든, 기록이든, 아니면 완전히 다른 무언가를 나타낼 때 새로운 메멘토들을 오리지네이터로부터 언제 요청해야 하는지, 이 메멘토들을 어떻게 저장하고, 언제 특정 메멘토로부터 오리지네이터를 복원해야 하는지를 알아야 합니다.
-
케어테이커들과 오리지네이터들 간의 연결은 메멘토 클래스로 이동시킬 수 있습니다. 이 경우, 각 메멘토는 자신을 생성한 오리지네이터와 연결되어야 합니다. 복원 메서드도 메멘토 클래스로 이동할 수 있습니다. 그러나 이 모든 것은 메멘토 클래스가 오리지네이터에 중첩되거나 오리지네이터 클래스가 메멘토 클래스의 상태를 오버라이드하기에 충분한 세터들을 제공하는 경우에만 의미가 있습니다.
장단점
- 캡슐화를 위반하지 않고 객체의 상태의 스냅샷들을 생성할 수 있습니다.
- 당신은 케어테이커가 오리지네이터의 상태의 기록을 유지하도록 하여 오리지네이터의 코드를 단순화할 수 있습니다.
- 클라이언트들이 메멘토들을 너무 자주 생성하면 앱이 많은 RAM을 소모할 수 있습니다.
- 케어테이커들은 더 이상 쓸모없는 메멘토들을 파괴할 수 있도록 오리지네이터의 수명주기를 추적해야 합니다.
- PHP, 파이썬 및 JavaScript와 같은 대부분의 동적 프로그래밍 언어에서는 메멘토 내의 상태가 그대로 유지된다고 보장할 수 없습니다.