DesignPatternSmalltalkCompanion:Memento
MEMENTO (DP 283)==
의도
캡슐화를 위배하지 않고 객체의 내부 상태를 캡처하여 외부로 노출시킴으로써, 나중에 객체를 그 상태로 되돌릴 수 있게 한다.
구조
논의
트랜잭션은 컴퓨터과학, 특히 비지니스 애플리케이션의 핵심 개념이다. 트랜잭션은 객체, 파일, 데이터베이스 같은 전산 엔티티(computational entity)에게 가해지는 복구 가능한 하나 또는 일련의 오퍼레이션으로서, 한 번의 수정(single reversible modification)으로 간주된다. 트랜잭션은 그것을 구성하는 모든 오퍼레이션이 성공하면 엔티티의 상태가 갱신되지만 오퍼레이션 하나라도 실패하면 모든 오퍼레이션의 결과가 버려져 엔티티의 상태가 트랜잭션 시작 전과 동일한 상태로 남는 속성을 가진다. 트랜잭션은 너무나 기본적인 개념이라서 데이터베이스라면 당연히 있을 것으로 기대되는 기능이다. Memento 패턴은 데이터베이스와는 상관없는(데이터베이스와 연관된 2차적인 트랜잭션도 있을 수 있겠지만 그것과는 관계없이) 객체의 트랜잭션에 관련된 패턴이다. 객체에 여러 업데이트를 수반하는 트랜잭션이 발생할 경우 문제가 생긴다. 하나 또는 그 이상의 업데이트를 실행 후 무언가 실패하면 객체를 어떻게 이전 상태로 돌릴 수 있을까?
이와 비슷하게, 대화형 애플리케이션에서는 사용자들이 사용자 인터페이스에서 실행한 작용이나 오퍼레이션을 undo하고자 할 때가 있다. 사용자가 실행의 결과를 보거나 자신이 실행한 오퍼레이션을 곱씹어 볼 시간이 있다면, 경우에 따라 사용자는 오퍼레이션을 다시 되돌려 해당 객체들을 이전 상태로 되돌리길 바랄 수도 있을 것이다. 이는 실패하지 않은 트랜잭션도 되돌릴 수 있어야 함을 의미한다.
따라서 우리는 객체의 상태를 회복시키거나 되돌릴 수 있는 방법이 필요하다. 객체를 이전 상태로 되돌리고자 하는 바람이 Memento 패턴의 동기이다. 이러한 바람은 다른 기본적 객체지향 개념인 캡슐화로 인해 복잡해진다. 객체지향 시스템에서 객체가 다른 객체의 내부 상태에 대해 직접 간섭하도록 허용하는 것은 좋지 못한 선택이다. 이에 우리는 진퇴양난에 빠진다: 캡슐화를 보존하면서 객체의 내부 상태를 저장하여 후에 필요할 때 복원할 수 있을까? Memento 패턴은 다음 3가지 역할을 정의함으로써 이에 대한 해법을 제시한다.
- Originator는 상태가 저장되어 회복될 수도 있는 객체이다.
- Memento는 Originator의 이전 상태를 "기억하는" 다른 객체이다.
- Caretaker는 Originator의 Memento를 챙기는 객체이다. 그것은 Originator에게 Memento를 언제 만들라고 말하고, Originator에게 Memento를 가지고 상태를 복원하라고 말한다.
구현
스몰토크로 Memento 패턴을 구현할 때 고려해야 할 몇 가지 사항들이 있다:
- 캡슐화 경계. [디자인 패턴] 편에서는 Memento 패턴에서 캡슐화 경계를 유지 시 긍정적인 결과를 가져온다고 설명한다 (DP 286). 또한 "어떤 프로그래밍 언어에서는 Originator가 아닌 다른 객체가 메멘토의 상태에 접근할 수 없도록 하는 것이 어려울 수 있다,"고 언급하였다. 스몰토크도 이러한 언어에 중 하나여서, 구체적인 구현을 숨기는 것이 힘든 일이 될 수도 있다. C++에서 우리는 외부 객체마다 서로 다른 인터페이스를 정의하는 선택권이 있다; 예를 들자면, friend 클래스에게는 넓은 범위의 인터페이스를, 다른 클래스에게는 좁은 인터페이스를 정의할 수 있다. 하지만 스몰토크에는 이러한 언어기반의 기능이 없다. 두 개의 인터페이스를 유지하는 것은 프로그래머의 능력에 맡겨야 한다.
[디자인 패턴] 편에 실린 메멘토패턴에 따르면, Memento 객체는 두 개의 인터페이스를 제공한다. Originator는 그 상태를 저장하고 회복하는데 필요한 메서드로의 접근이 허용된다 (이것을 "넓은" 범위의 인터페이스라고 한다). 그 메서드의 이름은 각각 setState와getState이다. 시스템 내의 그 외 어떤 객체도 이들 메서드를 접근할 수 없다. [디자인 패턴]에 따르면 Caretaker는 "좁은" 범위의 인터페이스를 보는데, 사실 이것은 전혀 인터페이스가 아니다. Caretaker는 Originator를 위해 Memento 객체를 보유하고 있을 뿐이다; Originator에게 Memento를 만들라고 명령하고, 그렇게 만들어진 Memento 객체를 받아 가지고 있다가, 복원이 필요하면 그것을 다시 Originator에게 전달한다. 결국, Caretaker의 "인터페이스"는 Memento 객체를 다른 객체들에게 주는 것 뿐인 셈이다. - 상태 접근. [디자인 패턴]은 Memento>>getState와 Memento>>setState 메시지들의 구현에 관해서는 구체적으로 설명하지 않았다. 예를 들어, 이 두 메시지에 대한 예제 코드를 전혀 소개하고 있지 않다. 그럼에도 불구하고 여기서는 이러한 메서드들이 스몰토크에서 어떻게 작동할지에 관한 예제를 최소한 하나라도 소개하고자 한다. 스몰토크에서는 적어도 Memento 객체에 그것의 내부 상태를 접근하기 위한 getter-setter 메서드를 구현하는 것이 필요할 것이다. Memento의 setState 메시지는 (DP 285) 파라미터가 없기 때문에 않기 때문에 말이 안 된다. Originator가 Memento에 상태를 저장하기 위해서는 메시지의 인자로 정보를 전달해야 한다. Originator 스스로를 파라미터로 Memento에게 전달하고, Memento는 Originator 객체에 메시지를 보냄으로써 자신이 필요로 하는 정보를 얻는 것이 한 가지 방법이 되겠다. 이번 사례에서 메서드 서명은 setState: 로서 다음과 같다:
Originator>>createMemento "Create a snapshot of my current state." | memento | memento := Memento new. memento setState: self. ^memento Memento>>setState: anOriginator "Get state from anOriginator." self stateVariable1: anOriginator stateVariable1; stateVariable2: anOriginator stateVariable2; stateVariable3: anOriginator stateVariable3
그러나 이 접근법은 특정 애플리케이션의 Originator 객체의 캡슐화를 깬다; 이 접근법은 Originator의 상태로 접근하게 해주는 (stateVariable1과 같은) 메서드를 구현하게 만든다. 이런 방법 말고, Memento가 애플리케이션 자체에 참여하는 객체보다 조금 덜 "알려진" 또 다른 유형의 인터페이스를 생각해 볼 수 있다. (Originator와 Caretaker만 Memento 객체를 안다). 그 방법에서는 (적어도 Memento 패턴의 구현할 목적으로) Originator에 접근자 메시지를 구현하지 않아도 된다. 여기서는 Memento가 stateVariableN: setter 메시지를 구현할 것이다:Memento>>getState를 구현할 때에도 비슷한 생각을 해볼 수 있다.Originator>>createMemento "Create a snapshot of my current state." | memento | memento := Memento new. memento stateVariable1: stateVariable1; stateVariable2: stateVariable2; stateVariable3: stateVariable3. ^memento
- Combination Originator와 Memento. 스몰토크에서 Memento 패턴의 적용할 때, Originator와 Memento 클래스를 구분하지 않으면 구현을 보다 단순화시킬 수 있다. Originator와 Memento가 서로 다른 클래스의 인스턴스여야 한다는 요구사항은 없다; 사실 동일한 클래스의 서로 다른 인스턴스가 Originator와 Memento의 역할을 수행하는 것이 더 말이 되는 경우가 종종 있다. 다시 말해 Originator와 Memento는 서로 다른 객체이면서 동일한 클래스의 멤버인 것이다.
스몰토크의 Object 의 copy 메서드를 사용하면 수신자의 “얕은 복사”를 할 수 있어서, 이 해법을 구현하기 쉽게 한다. 많은 경우, Originator의 얕은 복사만으로 충분히 Memento 역할을 시킬 수 있다. Originator의 상태를 표현하고 안전하게 보관하는 데에 얕은 복사만으로는 부족하다면, 깊은 복사를 사용할 수 있다. (Memento가 만들어진 후 Originator의 인스턴스 변수에 전송된 메시지로 인해 변화가 일어날 경우, 이 메시지들은 얕은 복사를 적용시킨 Memento의 변수도 변경시킬 것이다! 우리는 Memento가 구체적 시점에 Originator의 상태를 그대로 보존하길 원한다; Originator가 변경 시 Memento도 변한다면 Originator를 이전 상태로 되돌릴 수 없다. copy와 deepCopy에 대한 자세한 논의 내용은 Prototype (77) 패턴을 참조하라.)
이 해법과 고찰2을 연관지어 생각해 본다면: Memento 객체가 getter 접근 메서드를 구현해야만 Originator 객체가 메멘토의 상태에 대한 부분을 각각 복원하도록 (setMemento: 를 아래 예제 코드에서 참조) 해야 한다. 이것과 함께, Memento와 Originator 객체들이 같은 클래스의 인스턴스라고 했다; 이것을 종합해보면, Originator (읽는, 도메인의 또는 애플리케이션의) 객체의 내부 인스턴스 변수들을 노출시켜야만 한다. 하지만, 우리는 외부 객체에 getter 메시지만 노출시킬 뿐이다; 우리는 setter 메시지를 노출시키지 않는다. (따라서, 이번 구현에서 외부 객체들은 내부 변수를 설정할 수 없다). - 복합 객체 네트워크. Memento 패턴을 복합 객체 네트워크에 적용 시 발생 가능한 치명적인 문제는, 특정 시점에 정확히 같은 모습으로 재구성하는 것과 관련된다. 다수의 객체들이 서로 의존적일 경우 이 객체들은 한 번에 저장할 필요가 있다. 이것은 특히 앞 단락에서 논한 copy와 deepCopy 문제를 고려하면 복잡해질 수 있는 문제이다. 일부 사례에서는 객체가 실제 참조되는 객체가 아니라 특정 객체에 대해 참조를 갖고 있다는 사실만 기록하길 원한다. 이것은 메멘토가 복원될 때, 새 정보가 연관된 오래된 복사물로 덮어써지는 것을 방지할 것이다. 이와 같은 경우 Proxy 패턴이 해결책이 될 수 있다; Memento는 실제 객체 대신 객체에 대한 프록시를 가지고 있다가, Memento를 복원할 때, 객체들 간의 의존성도 함께 복구될 수 있도록 해준다.
예제 코드
Memento와 Originator의 클래스가 같은 간단한 예제를 살펴보자. 이 예제에서 본래 값으로 복구시키길 바라는 객체는, 2장에 소개된 바 있는, 보험회사의 고객을 뜻하는, Client이다:
Object subclass: #Client
instanceVariableNames: 'name address phone'
classVariableNames: ''
poolDictionaries: ''
클래스는 약간의 추가 프로토콜만을 구현하여 Memento 패턴을 지원해야 한다. 그 프로토콜은 Originator의 외부 프로토콜로서, createMemento와 setMemento: 메서드이다 (DP 286에 표시):
Client>>createMemento
"Save my current state in a memento (copy) of me."
^self copy
Client>>setMemento: aMemento
"Set my state to the state saved in aMemento."
name := aMemento name.
address := aMemento address.
phone := aMemento phone.
Caretaker는 이 두 메시지를 각각 Memento의 생성과 Memento로부터 객체 상태를 복구시키기 위한 목적으로 전송한다. (어찌되었건 setMemento: 라는 메시지 이름은 혼란스럽다; restoreFromMemento: 가 더 적절할 것이다.) Memento를 Originator의 복사본으로 두게 되면 [디자인 패턴] 편의 패턴 버전에서와 같이 Originator가 Memento로 메시지 setState를 전송할 이유가 없어진다.
이번 예제에서는 Caretaker가 Client 편집기로서 대표되는 애플리케이션 모델이 되게 하였다. 애플리케이션 모델에 있는 newSelection 메서드는 사용자가 목록에서 새 클라이언트를 선택할 때마다 호출된다; 이때 애플리케이션 모델은 다른 사용자 변경사항이 있기 전에 클라이언트의 상태를 저장하기 위해 새로운 메멘토를 생성한다. Undo 메뉴를 선택 시 사용자가 실행한 편집을 되돌리는 undoChange 메서드를 호출한다:
ClientListHolder>>newSelection
"Tell the newly selected Client (if any) to save off
its state before the user edits it."
self domainObject notNil ifTrue:
[self memento: self domainObject createMemento].
...
ClientListHolder>>undoChanges
(self domainObject isNil
or: [self memento isNil]) ifFalse:
[self domainObject setMemento: self memento].
위는 도메인 객체의 상태를 복구하는데 필요한 모든 코드다. 새로 변경된 도메인 객체를 표시하기 위해 사용자 인터페이스의 상태를 복구하는 데에는 다른 코드가 필요할지도 모른다.
이번 예제의 구조 다이어그램은 다음과 같은 모습이다:
알려진 스몰토크 사용예
주문 관리 시스템
Brown (1996)은 클래스 하나로 메멘토 패턴의 세 참가자인, Caretaker, Memento와 Originator를 모두 구현하는 메멘토 패턴을 사용한 것에 대해 설명하였다. 이는 메멘토 패턴의 가장 기형적인 경우일 것이다. 그러나 캡슐화를 완전하게 지켜낼 수 있다는 독보적인 이점이 있긴 하다. 외부 객체는 Memento의 존재를 알 필요가 없어지고, 메멘토 패턴을 구현하는 것이 유일한 목적인 접근자 메서드를 작성할 필요가 없어진다.
리팩토링 브라우저
스몰토크 클래스 계층구조를 리팩토링하는 도구인 Smalltalk Refactoring Browser (Roberts, Brant, & Johnson, 1997a, 1997b)는 편집 버퍼들을 서로 교환(switch)하기 위해 Memento 패턴을 사용한다. 한 번에 여러 Memento를 사용하여 각 버퍼의 상태가 다른 편집세션이 되도록 구현하여 흥미롭다.
GF/ST 그래픽스 프레임워크
GF/ST는 Polymorphic Software (현재 ParcPlace-Digitalk의 부서)가 개발한 직접 조작 가능한 그래픽을 위한 상업적 스몰토크 프레임워크로서, Memento 패턴을 이용해 undo 기능을 구현한다. Memento 패턴의 구현은 C++에 유지되는 privacy 제약의 일부를 유지한다. Memento 클래스는 메멘토로부터 어떤 객체를 복구시킬 수 있는지를 결정하는 accessKey라는 변수가 추가된다. accessKey 가 nil일 경우 복구될 객체는 originator가 된다 (==가 참이 되어야 한다). nil이 아닌 경우 accessKey는 메시지 셀렉터로 사용될 심볼이다. originator와 복구될 객체는 모두 이 메시지에 응답하며, 그들이 리턴하는 두 값은 같아야 한다.
GF/ST의 Memento 구현에서는 시발자의 상태를 복구시키기 위해 Object>>perform:with:를 사용한다. 메멘토는 상태 값과 그 값을 복구하는데 사용된 메시지를 모두 기억한다. 후에는 perform:with: 를 사용하여 상응하는 값을 가진 각 복구 메시지를 호출한다.
이 코드는 (ParcPlace-Digitalk의 Jasen Minton이 제시한) Memento 패턴이 어떻게 작용하는지를 보여준다:
| aMemento |
someRectangle := Rectangle origin: (0@0) extent: (25@50).
aMemento := Memento
originator: Rectangle new
state: someRectangle extent copy
type: #extent:
accessKey: #class.
이렇게 하고난 Rectangle someRectangle 은 변경, 변형, 또는 쓰레기로 수집이 되어도 된다. 그렇게 된다하더라도, 후에 동등한 Rectangle 객체의 상태를 메멘토로부터 복원시킬 수 있다. anotherRectangle이 Rectangle의 인스턴스라고 가정할 때 (현재 인스턴스 변수에 무엇이 지정되어 있는지는 상관이 없다), 다음 코드 한 줄을 실행하면 그 객체(anotherRectangle)를 이전 상태로 복구된다.
aMemento restore: anotherRectangle
관련 패턴
Memento 패턴과 Command 패턴
때로는 undo 문제를 해결하는 데에 있어 Memento 패턴이 최선의 해법이 아닐 수 있다. 객체에 일어난 변화가 심각한 부작용을 일으켜 그 상태의deepCopy가 필요한 경우에는 Memento 패턴이 주는 이익보다 골치거리가 더 클 수도 있다. 이런 경우 Command 패턴이 더 나은 해법을 제공한다. (undo-redo 리스트와 관련된 논의는 Command 패턴을 참조한다(245)).
"What If?"
"What If?" 프로토콜은(Griffin, 1993) Memento 패턴을 좀더 복잡하게 한 변종이다. 이 변종을 이해하기 위해서, Memento 패턴이 대략 다음과 같은 절차로 진행된다고 생각하자.
- Originator의 Memento를 만든다.
- Originator에 취소할 필요가 있을 수 있는 조작을 가한다.
- Originator의 상태를 검증한다 (이 검증은 프로그램이 하는 내부 검사일 수도 있고, 사용자가 하는 외부 검사일 수도 있다).
- 필요 시 Memento로부터 Originator의 상태를 복구한다.
Griffin은 약간 다른 접근법은, 실제 대상 객체를 검증하는 것이 복잡하거나, 조작이 가해진 대상 객체를 되돌리는 것이 어려울 때, 특히 유용하다. Griffin은 다음과 같은 수정된 절차를 제시한다:
- Originator의 복사본을 만든다.
- 복사본에 조작을 가한다.
- 조작들이 복사본의 내부 상태를 잘못되게 만들었는지 판단한다.
- 잘못이 있다면, 복사본을 제거하고 예외가 발생시킨다. (Originator는 변경되기 전 상태로 둔다!).
- 잘못이 없다면, Originator에 조작을 가한다.
이러한 대안적 절차는 몇 가지 흥미로운 결과를 수반한다. 이 방법의 최대 장점은 Originator가 비정상적인 상태가 되는 경우가 없고, 그것의 동일성과 그에 포함된 객체들의 동일성이 유지된다는 점이다. 이는 영속 메커니즘이 관련 객체들의 동일성에 따라 결정될 때 특히 중요하다. 불행히도 당신은 성공적인 조작을 두 번 실행해야 한다. 이 접근법은 Command 패턴과 비슷하지만, Command 패턴은 대상 객체에 한 번에 하나의 조작만 실행된다는 점에서 다르다.