DesignPatternSmalltalkCompanion:Memento

From 흡혈양파의 번역工房
Revision as of 08:33, 1 August 2012 by Onionmixer (talk | contribs) (DPSC MEMENTO 페이지 추가)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

MEMENTO (DP 283)==

의도

캡슐화를 위배하지 않고 객체 내부 상태를 객체화하여, 후에 객체를 이 상태로 복구 가능하게 한다.

구조

Dpsc chapter05 Memento 01.png

논의

컴퓨터과학, 특히 비지니스 애플리케이션에서 기본 개념은 트랜잭션이다. 트랜잭션은 전산 엔티티(computational entity)에서 실행되는 하나의 또는 일련의 오퍼레이션으로서 (객체, 파일, 데이터베이스) 엔티티에 대한 복구 가능한 한 번의 수정(single reversible modification)으로 간주된다. 트랜잭션은 그 오퍼레이션이 모두 성공하여 엔티티의 상태가 업데이트된 속성을 가지거나, 어느 하나의 오퍼레이션이라도 실패 시 다른 오퍼레이션들의 결과가 폐기되어 엔티티의 상태가 트랜잭션 시작 전과 동일한 상태로 남아 있는다. 트랜잭션은 모든 데이터베이스 시스템의 landscape의 일부로 가정한다는 점에서 기본적 개념이라 할 수 있다. 그러나 Memento 패턴은 설사 데이터베이스에 대해 관련된, 이차적 트랜잭션이 있다 하더라도 객체에는 득이 될 것이 없는 트랜잭션이다. 객체에 여러 업데이트를 수반하는 트랜잭션이 발생할 경우 문제가 생긴다. 하나 또는 그 이상의 업데이트를 실행 후 무언가 실패하면 객체를 어떻게 이전 상태로 돌릴 수 있을까?

이와 비슷하게, 대화형 애플리케이션에서는 사용자들이 사용자 인터페이스에서 실행한 작용이나 오퍼레이션을 undo하고자 할 때가 있다. 사용자가 실행의 결과를 보거나 자신이 실행한 오퍼레이션에 반영할 시간이 있을 경우 사용자는 오퍼레이션을 다시 되돌리고 해당 객체는 이전 상태로 되돌리길 바랄 것이다. 이는 트랜잭션이 실패하더라도 원상태로 되돌릴 수 있음을 의미한다.

따라서 우리는 객체의 상태를 회복시키거나 되돌릴 수 있는 방법이 필요하다. 객체를 이전 상태로 되돌리고자 하는 바람이 Memento 패턴의 동기이다. 이러한 바람은 다른 기본적 객체지향 개념인 캡슐화로 인해 복잡해진다. 객체지향 시스템에서 객체가 다른 객체의 내부 상태에 대해 직접 간섭하도록 허용하는 것은 좋지 못한 선택이다. 이에 우리는 진퇴양난에 빠진다: 캡슐화를 보존하면서 객체의 내부 상태를 저장하여 후에 복귀(fallback)가 필요할 때 되찾을 수 있을까? Memento 패턴은 다음 3가지 역할을 정의함으로써 이에 대한 해법을 제시한다.

  1. Originator는 상태가 저장되어 회복될 수도 있는 객체이다.
  2. Memento는 Originator의 이전 상태를 “기억하는” 다른 객체이다.
  3. Caretaker는 Originator를 위해 Memento를 기록하는 객체이다. 이것은 메멘토를 언제 생성할지와 메멘토로부터 상태를 언제 복구해야 할지를 Originator에게 알려준다.

구현

스몰토크 특유의 Memento 패턴을 구현 시에 고려해야 할 몇 가지 문제가 있다:

  1. 캡슐화 경계. [디자인 패턴] 편에서는 Memento 패턴에서 캡슐화 경계를 유지 시 긍정적인 결과를 가져온다고 설명한다 (DP 286). 또한 “어떤 프로그래밍 언어에서는 Originator 클래스가 메멘토의 상태에 접근하기 위해 필요한 인터페이스만 사용하도록 제한함을 보장하기 어려울 수 있다,”고 언급하였다. 스몰토크도 이러한 언어에 속하므로 구현부의 정보를 숨겼을 때 원하는 결과를 얻기란 도전적인 일이 될 수도 있다. C++에서 우리는 외부 객체마다 서로 다른 인터페이스를 정의하는 선택권이 있다; 클래스의 friends에게 제공하는 넓은 범위의 인터페이스와 다른 클래스에게 제공하는 좁은 인터페이스를 정의할 수 있는 것이다. 하지만 스몰토크에는 이러한 언어기반의 기능이 없다. 두 개의 인터페이스를 유지하는 것은 프로그래머의 능력에 맡겨야 한다.
    [디자인 패턴] 편에 실린 패턴 버전에서 Memento 객체는 두 개의 인터페이스를 제공할 것을 제안한다. Originator는 그 상태를 저장하고 회복하는데 필요한 메소드로의 접근이 허용된다 (“넓은” 범위의 인터페이스). 이 메소드의 이름은 각각 setState와getState이다. 시스템 내의 그 외 어떤 객체도 이러한 메소드로 접근을 허용해선 안 된다. [디자인 패턴]에 따르면 Caretaker는 “좁은” 범위의 인터페이스를 보지만 이것은 사실 전혀 인터페이스가 아니다. Caretaker는 Originator를 위해 Memento 객체를 보유하고 있을 뿐이다; Originator에게 Memento의 생성을 알려주어 Memento 객체를 받고, 회복이 필요하면 다시 Originator에게 전달한다. 따라서 그것의 “인터페이스”는 Memento와 다른 객체들로 전달할 수 있는 기능이 있을 뿐이다.
  2. 상태 접근. [디자인 패턴]은 Memento>>getState와 Memento>>setState 메시지들을 구현하는 방식에 대해서는 애매하다. 예를 들어, 이 두 메시지에 대한 예제 코드를 전혀 소개하고 있지 않다. 그럼에도 불구하고 여기서는 이러한 메소드들이 스몰토크에서 어떻게 작동할지에 관한 예제를 최소한 하나라도 소개하고자 한다. 스몰토크에서 내부 상태를 위한 getter-setter 메소드를 구현하기 위해서는 최소한 Memento 패턴이 필요할 것이다. [BR]]Memento의 setState 메시지는 (DP 285) 파라미터를 취하지 않기 때문에 유명무실이라 할 수 있다. Originator가 Memento에 상태를 저장하기 위해서는 메시지 argument 형태로 된 정보를 전달해야 한다. Originator 스스로를 파라미터로 전달하고, 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 가 이러한 유형의 인터페이스를 표현하도록 하겠다 (Memento 패턴은 Originator와 Caretaker에게만 알려져 있다). 그리고 나면 Originator를 제외한 (적어도 Memento 패턴의 구현 목적으로는) Memento만 접근자 메시지를 구현하면 된다. 여기서는 Memento가 stateVariableN: setter 메시지를 구현할 것이다:
    Originator>>createMemento
    	"Create a snapshot of my current state."
    	| memento |
    	memento := Memento new.
    	memento
    		stateVariable1: stateVariable1;
    		stateVariable2: stateVariable2;
    		stateVariable3: stateVariable3.
    	^memento
    
    Memento>>getState를 구현할 때에도 비슷한 우려 요인이 있다.
  3. . Combination Originator와 Memento. 스몰토크에서 Memento 패턴의 적용을 단순화시킬 수 있는 구현은 Originator와 Memento 클래스의 차이를 없애는 데에 좌우된다. Originator와 Memento가 서로 다른 클래스의 인스턴스여야 한다는 요구사항은 없다; 사실 동일한 클래스의 서로 다른 다른 인스턴스가 Originator와 Memento의 역할을 수행하는 것이 더 말이 된다. 다시 말해 Originator와 Memento는 서로 다른 객체이면서 동일한 클래스의 멤버인 것이다.
    스몰토크는 Object에 구현되는 메소드이자 수신자의 “얕은 복사”를 리턴하는 copy 메소드를 제공함으로써 이 해법을 구현하기 수월하게 만든다. 많은 사례에서 Originator의 얕은 복사만으로 Memento 패턴의 역할을 다한다. Originator의 상태를 표현하고 안전하게 보관하는 데에 얕은 복사 이상이 필요하다면 깊은 복사가 요구된다. (Memento가 Originator의 인스턴스 변수를 저장 후 그들에게 전송된 메시지로 인해 변화가 일어날 경우, 이 메시지들은 얕은 복사를 적용시킨 Memento도 변경시킬 것이다! 우리는 Memento가 구체적 시점에 Originator의 상태를 snapshot 하길 원한다; Originator가 변경 시 Memento도 변한다면 Originator를 이전 상태로 되돌릴 수 없다. copy와 deepCopy에 대한 자세한 논의 내용은 Prototype (77) 패턴을 참조한다.)
    그러나 이 해법을 2번에 관련시켜보자: Memento 객체는 getter 접근 메소드를 구현하여 Originator 객체가 그의 상태에 대한 각 부분(portion)을 회복하도록 (setMemento: 를 아래 예제 코드에서 참조) 함과 동시 Memento와 Originator 객체들이 동일한 클래스의 인스턴스가 되도록 한다; 따라서 우리는 최종적으로 Originator (읽기, 도메인 또는 애플리케이션) 객체의 내부 인스턴스 변수들을 노출시킨다. 하지만 외부 객체에 getter 메시지만 이용 가능하도록 만든다; setter 메시지를 public으로 만들고 싶지 않기 때문이다 (이에 따라 이번 구현에서 outsider들은 내부 변수를 설정하지 않을지도 모른다).
  4. 복합 객체 네트워크. Memento 패턴을 복합 객체 네트워크에 적용 시 발생 가능한 치명적인 문제는, 특정 시점에 정확히 같은 모습으로 재구성하는 것과 관련된다. 다수의 객체들이 서로 종속적일 경우 이 객체들은 한 번에 저장할 필요가 있다. 이것은 특히 앞 단락에서 논한 copy와 deepCopy 문제를 고려하면 복잡해질 수 있는 문제이다. 일부 사례에서는 객체가 실제 참조되는 객체가 아니라 특정 객체에 대해 참조를 갖고 있다는 사실만 기록하길 원한다. 이는 관련된 Memento가 복구될 때 오래된 copy가 새로운 정보를 덮어쓰지 못하도록 막아줄 것이다. 이와 같은 경우 Proxy 패턴이 해법을 제시할 수 있다; Memento는 실제 객체 대신 객체에 대한 프록시를 유지하여 후에 Memento를 복구 시 종속성도 복구될 수 있도록 해준다.

예제 코드

Memento와 Originator가 동일한 클래스 출신인 경우의 간단한 예제를 살펴보자. 이 예제에서 본래 값으로 복구시키길 바라는 객체는 Client의 인스턴스로, 2장에 소개한 보험회사의 클라이언트를 나타낸다:

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를 애플리케이션 모델로 만들어 클라이언트 편집기를 표현하고자 한다. 애플리케이션 모델에 있는 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].

위는 도메인 객체의 상태를 복구하는데 필요한 모든 코드다. 새로 변경된 도메인 객체를 표시하기 위해 사용자 인터페이스의 상태를 복구하는 데에는 다른 코드가 필요할지도 모른다.

이번 예제의 구조 다이어그램은 다음과 같은 모습이다:

Dpsc chapter05 Memento 02.png

알려진 스몰토크 사용예

주문 관리 시스템

Brown (1996)은 Memento 패턴을 이용해 한 클래스가 3개의 참가자, Caretaker, Memento와 Originator를 모두 구현하는 사용예를 설명하였다. 이는 Memento 패턴의 가장 퇴보적 경우일 것이다. 그러나 캡슐화를 완전히 보호한다는 이점이 있긴 하다. 외부 객체는 Memento의 존재를 알 필요가 없으며, 우리는 패턴에 대한 외부-Memento 버전을 구현하는 것을 목적으로 한 접근자 목적의 필요성을 제거하였다.

리팩토링 브라우저

스몰토크 클래스 계층구조를 리팩토링하는 도구인 Smalltalk Refactoring Browser (Roberts, Brant, & Johnson, 1997a, 1997b)는 편집 버퍼들을 서로 교환(switch)하기 위해 Memento 패턴을 사용한다. 한 번에 여러 Memento를 사용하여 각 버퍼의 상태가 서로 다른 편집 세션이 된다는 점에서 눈에 띄는 구현이라 할 수 있다.

GF/ST 그래픽스 프레임워크

GF/ST는 Polymorphic Software (현재 ParcPlace-Digitalk의 부서)가 개발한 직접조작 그래픽스를 위한 상업적 스몰토크 프레임워크로서, Memento 패턴을 이용해 unto 기능을 구현한다. 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.

someRectangle의 Rectangle은 변경, 변형, 또는 쓰레기로 수집이 가능하다. 그러나 후에 동등한 Rectangle 객체를 메멘토로부터 복귀시킬 수 있다. anotherRectangle이 Rectangle에 속한다고 가정할 때 (최근 인스턴스 변수가 어디로 설정되었는지는 중요치 않다), 다음 코드 라인이 본래 상태에 복구될 것이다:

aMemento restore: anotherRectangle

관련 패턴

Memento 패턴과 Command 패턴

때로는 undo 문제를 해결하는 데에 있어 Memento 패턴이 최선의 해법이 되기도 한다. 객체에 일어난 변화가 유의한 부작용을 일으켜 그 상태의deepCopy가 필요한 경우에는 Memento 패턴이 더 큰 문제가 되기도 한다. 이런 경우 Command 패턴이 더 나은 해법을 제공한다. (undo-redo 리스트와 관련된 논의는 Command 패턴을 참조한다(245)).

“What If?”

“What If?” 프로토콜은(Griffin, 1993) Memento 패턴의 복잡한 변체이다. 이를 이해하려면 Memento 패턴이 대략 다음 오퍼레이션 순서대로 담당한다고 생각하면 된다.

  1. Originator의 Memento를 만든다.
  2. Originator에서 취소할 필요가 있는 오퍼레이션을 일부 실행한다.
  3. Originator의 상태를 검증한다 (내부 검사나 사용자에 의한 외부 검사가 될 수 있다).
  4. 필요 시 Memento로부터 Originator의 상태를 복구한다.

Griffin은 약간 다른 접근법이지만 실제 대상 객체에 대해 복잡한 검증이 있거나 실제 대상 객체에 오퍼레이션을 실행할 때 특히 유용한 접근법에 있는데 후에 복구가 까다로울 수 있다고 설명한다. Griffin은 대신 다음과 같은 오퍼레이션 순서를 제시한다:

  1. Originator의 copy를 만든다.
  2. copy에 오퍼레이션을 수행한다.
  3. 오퍼레이션이 copy의 내부 상태를 무효화할 수 있는지 결정한다.
  4. 무효화가 가능한 경우 copy를 제거하고 예외가 발생하였다고 말한다 (Originator는 변경되기 전 상태로 둔다!).
  5. 그리고 Originator에 오퍼레이션을 실행한다.

이러한 대안적 순서는 몇 가지 흥미로운 결과를 수반한다. 주요 이점은 Originator의 상태가 항시 일관성이 있고, 그 정체성과 그에 포함된 객체들의 정체성이 유지된다는 점이다. 이는 유지되는 관련 객체들의 정체성에 따라 영속 메커니즘이 좌우될 때 특히 중요하겠다. 불행히도 당신은 오퍼레이션을 성공적으로 두 번 실행해야 한다. 이러한 접근법은 Command 패턴과 비슷하지만 실제 대상 객체에 한 번에 하나의 오퍼레이션만 실행된다는 점에서 차이가 있다.

Notes