DesignPatternSmalltalkCompanion:Proxy

From 흡혈양파의 번역工房
Jump to navigation Jump to search

PROXY (DP 207)

의도

다른 객체로의 접근을 통제하기 위해서 다른 객체의 대리자 또는 다른 객체로의 정보 보유자를 제공한다.

구조

File:Dpsc chapter04 Proxy 01.png

논의

객체지향 디자인에서 중요한 부분은 어떠한 객체가 다른 어떤 객체를 아는지 결정하는 것이다. 스몰토크에서 이러한 연관성은 주로 객체로의 직접 참조 또는 Observer 패턴과 같은 일반적 메커니즘을 통해 이루어진다. 하지만 때로는 다른 실제적 객체를 아예 참조하고 싶지 않을 때가 있다. 대신 “이러이러한 종류의 객체는 여기에 있어야 한다,”라고 말하고 실제 객체에 대한 대리자를 참조하는 경우가 있다. 이러한 바람이 바로 Proxy 패턴의 동기이다.

Proxy 패턴에서 핵심은 동일한 타입의 두 개의 객체, 즉 플레이스홀더와 완전히 보호된 객체에 있는데, 여기서 플레이스홀더는 자격을 갖춘 객체로 접근을 통제한다. 플레이스홀더 Proxy는 완전히 보호된 객체 RealSubject와 같은 인터페이스를 가지므로 나머지 시스템은 RealSubject를 사용하고 있다고 생각하지만 사실상 Proxy를 사용하고 있는 것이다.

Proxysms 제한된 상태와 행위를 이용해 가능한 한 많은 메시지를 처리하여, 그것이 RealSubject로 메시지를 전달해야 하는 경우에만 전달을 맡는다. 이는 Proxy가 시스템의 RealSubject 사용을 제한시키거나 최적화시킬 수 있도록 해준다.

스몰토크에서 사용되는 Proxy 패턴

Proxy는 스몰토크에서 흔히 사용되는 패턴으로, 두 가지로 이유로 사용된다:

  1. 영속적 프록시. 관계형 데이터베이스나 객체 데이터베이스 또는 플랫파일과 같은 영속적 저장소에서 객체를 로딩하는 것은 시간이 많이 소요되는 것이 보통이다. 수 많은 영속 객체, 특히 절대 사용하지 않는 영속 객체를 로딩하는 것은 시간이 많이 소요되지 않을진 모르지만 낭비라는 점은 매한가지다. 영속적 객체를 위한 프록시는 전혀 사용되지 않는 객체의 로딩을 피하게 해준다. 거의 모든 상업적 데이터베이스 프레임워크가 이러한 형태의 Proxy 패턴을 최소한 하나씩은 포함한다. 객체 데이터 벤더들의 스몰토크 언어 바인딩 대다수도 이를 사용한다. 이것을 [디자인 패턴] 용어로 가상 프록시라고 한다.
  2. 분산 프록시. 논리적 스몰토크 이미지가 물리적 이미지들에 걸쳐 분산될 때 각 객체는 하나의 물리적 이미지에 존재할 수 있지만 다른 이미지들로부터 접근할 수 있어야 한다. 물리적 이미지들에 걸친 메시지 송신은 이미지 내의 메시지 송신보다 느리다. 원격 객체로의 프록시는 메시지 송신을 최소화시킴으로써 협력을 최적화시킬 수 있고, 이에 따라 원격 객체가 로컬 객체에 가깝게 행동하도록 만들 수 있다. 시장에 대한 분산 스몰토크, VisualAge Distributed Option과 ParcPlaceDigitalk Distributed Smalltalk 모두 이 패턴을 사용한다. [디자인 패턴]에서는 이를 원격 프록시라 일컫는다 (DP 208).

[디자인 패턴] 편에서는 두 가지 유형의 프록시, 즉 보호용 프록시와 smart 참조를 언급한다. 하지만 이러한 프록시들은 스몰토크에서는 다른 언어에서만큼 흔히 사용되지 않는다. 본 저서의 논의 부분에서는 앞서 언급하였던 프록시 유형 두 가지를 중점으로 살펴보고자 한다.

구현=

(거의) 모든 것을 전달하는 프록시

Proxy 패턴을 구현하는 데에는 크게 2가지 접근법이 있다. [디자인 패턴] 편에서는 추상 클래스 Subject에 RealSubject와 Proxy를 서브클래스를 둔 접근법을 구현하는데 중점을 둔다. Proxy는 Subject의 모든 메시지를 지원하여 처리할 수 있는 것은 처리하고 나머지 RealSubject로 전달한다.

스몰토크의 역동적 바인딩은 [디자인 패턴] 에서 언급한 내용에 (DP 212-213, 215-216) 대안책을 제공한다. Proxy는 RealSubject의 인터페이스를 전부 지원하기보다는 일부만 구현하며, doesNotUnderstand: 를 이용해 다른 메시지를 그것의 RealSubject로 전달한다. 여기서 중요한 고려사항 한 가지는, Proxy는 그 슈퍼클래스로부터 메시지를 상속받아선 안 된다는 점이다. 그것이 수신할 메시지마다 Proxy는 그에 해당하는 메소드를 구현해야 하는데, 그렇지 않으면 doesNotUnderstand:가 호출되도록 해야 한다. 안타깝게도 대부분 다른 클래스들과 마찬가지로 Proxy도 Object의 자손(descendent)이다. 즉 그것이 메소드를 상속하여 명시적으로 재구현하고 싶어하지 않을 수도 있음을 의미한다 (printOn:, inspect 등).

Object와 Proxy 서브클래스의 관계에 문제가 있기 때문에 이를 해결할 한 가지는 이 관계를 해제하는 것이겠다. 이는 어떻게 가능할까? Proxy를 nil로부터 상속하기. 그렇다, 스몰토크에서는 이것이 가능하다. 결국은 무엇이 Object의 슈퍼클래스인가? 바로 nil이다. nil이 하나의 서브클래스를 가질 수 있다면 다른 서브클래스들을 가질 수도 있으나 결과는 꼭 있어야 한다.

스몰토크의 간단한 일관성들 중 하나는 모든 것이 객체라는 점이다. 이는 모든 것이 메시지를 수신할 수 있는 추상 데이터 타입임을 의미할 뿐 아니라 모든 것은 한 클래스의 인스턴스이고 모든 클래스는 Object의 서브클래스이다 (Object 자체만 제외하고). Proxy에 대한 이러한 접근법은 단순한 진실을 깬다. Proxy들은 여전히 객체이지만 Object의 종류는 아니다.

전달해선 안 되는 메시지

일반적으로 클래스를 nil의 서브클래스로 정의하는 것은 스몰토크의 객체들에 대한 많은 기본적 가정에 위반되기 때문에 위험하다. 예를 들어, 비주얼 스몰토크에서는 미묘한 버그로 인해 새로운 브라우저를 열기 전까지는 Class Hierarchy Brower에 있는 nil에 대해 새로 정의된 서브클래스를 볼 수 조차 없다. nil의 서브클래스와 같이 Proxy가 이해하는 유일한 메시지는 더블 이퀄인데, 그 이유는 더블 이퀄은 메시지로서 전송되는 대신 가상 머신에 의해 직접 구현되기 때문이다. Proxy는 doesNotUnderstand: 를 구현해야 하며, 그렇지 않으면 그것이 구현하지 않는 메시지는 모두 무한 루프를 야기한다.[1] 더블 이퀄을 제외한 모든 메시지의 경우, Proxy는 메시지를 스스로 구현하거나 doesNotUnderstand: 가 RealSubject로 메시지를 전달하도록 선택할 수 있다.

Proxy가 구현할 필요가 있는 몇 가지 다른 메소드를 소개하겠다:

  • isProxyㅡ이것은 객체가 프록시인지 아닌지를 말해줄 수 있는 간단한 방법을 제공한다. inspect를 사용하거나 printString에 의존해선 안 되는데 그 이유는 두 메소드 모두 주제대상으로 전송될 것이기 때문이다. Proxy 내에 존재하고 true를 반환하는 isProxy 구현부와 Subject에 존재하고 false를 반환하는 isProxy의 구현부가 필요할 것이다.
  • become:ㅡ이것은 설계에서 Proxy에 대해 RealSubject를 전환하고 싶을 때 구현해야 한다 (Proxy를 위한 RealSubject의 스와핑은 아래를 참조).
  • =과 hashㅡProxy를 비교하거나 Set 또는 다른 Collection에 저장할 경우 이 메시지들을 구현한다.[2]
  • halt와 vmInterrupt: (비주얼 스몰토크)ㅡ프록시를 디버깅할 계획일 경우 이 메시지들을 구현한다.
  • classㅡProxy가 그것의 클래스 측 메소드로 접근할 경우 이 메시지를 구현한다.

이 메시지들을 구현하려면 Object의 구현으로부터 코드를 복사한다.

RealSubject와 그 프록시의 스와핑

Proxy의 결점은 그들이 발생시킨 오버헤드로서, 특히 성능을 향상시키도록 된 경우에 더 그러하다. 예를 들어, 영속적 Proxy는 사실상 확실히 필요하기 직전까지는 영속 객체의 로딩을 피하기 때문에 처음부터 오버헤드를 감소시킨다. 하지만 영속 객체가 로드되는 순간부터 Proxy는 방해가 되기 시작한다. 이중으로 전송되는 메시지의 성능 저하가 적고 Proxy는 아마도 그다지 많은 메모리를 차지하진 않을 것이다. 하지만 일부 애플리케이션에서 오버헤드가 상당할 수 있으며, 설계사는 RealSubject를 이용할 수 있는 순간부터 Proxy를 제거하고 싶을 것이다.

이러한 제거 작업은 become: 이라 불리는 메시지를 사용해 완료할 수 있다. become:이 하는 일은 수신자를 향한 모든 포인터를 argument로서 통과하는 객체를 향하도록 변경하는 것이다. 이는 한방향 become: 으로서 비주얼 스몰토크에서 사용된다. 따라서 become: 메소드의 실행 후 수신자를 참조하는데 사용된 모든 객체들은 이제 argument를 가리키게 된다. 비주얼웍스와 IBM 스몰토크에서는 become: 이 양방향이기 때문에 argument를 참조하는 것은 모두 수신자를 가리킨다: 시스템 내에서 객체들 중 하나를 한 번이라도 가리킨 적이 있는 변수는 이제 모두 다른 객체를 가리킬 것이다. 이와 관련된 실행을 살펴보려면 다음 작업공간을 실행해본다:

| string1 string2 |
string1 := 'abc' copy.
string2 := 'xyz' copy.
string1 become: string2.
Transcript show: string1.	"Displays 'xyz'"
Transcript show: string2.
	"Displays 'abc' in VisualWorks and IBM"
	"Displays 'xyz' in Visual Smalltalk"

become: 은 무엇을 반환하는가? 메소드는 기본적으로 하나의 절차이기 때문에 self를 반환한다. 문제는, self가 무엇인가이다. 확실히 수신자는 아니다! 수신자를 향한 모든 포인터가 이제 self를 포함해 argument를 향하고 있고, become:은 수신자를 반환하는데 지금 수신자는 argument 이므로 argument를 반환한다고 보면 되겠다:

| string1 string2 string3 |
string1 := 'abc' copy.
string2 := 'xyz' copy.
string3 := string1 become: string2.
string3 printString		"Returns 'xyz'"

RealSubject에 대해 Proxy를 갖고 있고 RealSubject를 사용하기 위해 Proxy를 제거하고자 할 경우, 이를 교환하기 위해 become:을 사용할 수 있다.

aProxy become: aRealSubject

결국 모든 Proxy의 클라이언트들은 RealSubject를 참조한다. become: 이 한 방향이라면 Proxy는 여전히 RealSubject를 참조할 것이다. become: 이 양방향이라면 Proxy는 이제 스스로를 그 RealSubject로서 참조할 것이다. 어떤 이벤트건 Proxy가 become: 이전에 RealSubject로의 유일한 참조를 포함할 경우, 지금 어떠한 것도 Proxy를 참조하지 않으므로 쓰레기로서 수집되겠다.

doesNotUnderstand:와 become: 의 시사점

doesNotUnderstand:와 become: 모두 좋든 나쁘든 디자인에 큰 영향을 미친다. Beck (1997)이 언급하였듯 become: 은 디자인을 이해하기 힘들게 만들고 코드의 디버깅을 까다롭게 만들기 때문에 일반적으로 피해야 한다. doesNotUnderstand:의 경우도 마찬가지다. 게다가 이 메시지들은 빠른 메시지가 아니기 때문에 성능에 손실을 가져온다.

Proxy로 doesNotUnderstand:를 사용할 경우 두 가지 이점이 있다:

  1. 이것은 Proxy 내의 코드를 단순화시켜 작성할 코드가 줄어든다.
  2. 이것은 유연하며, 인터페이스 변경을 투명하게 처리한다.

여기에는 두 가지 단점도 있다:

  1. 디버깅하기가 힘들다. 그들을 구현하지 않는 객체들이 처리한 메시지를 보는 것은 충격이다.
  2. 직접 메시지 송신보다 느리다.

doesNotUnderstand:의 힘에 도취된 프로그래머들이 이를 남용하여 너무나도 일반적이고 비효율적인 결과를 낳게 되었다. 스몰토크는 메시지 송신에 최적이라는 사실을 기억하고, 이러한 목적에 가능한 한 많이 사용한다. 일반 메시지 송신 대신 doesNotUnderstand: 를 사용 시 성능적 측면에서 상당한 불이익도 발생하기도 한다. 비주얼 스몰토크에서는 그 차이가 미묘하지만 IBM 스몰토크에서는 doesNotUnderstand: 가 3배 정도 느리며, 비주얼웍스에서는 6배 가량이 느리다.

become: 메소드 또한 느리게 실행될 수 있다. 사실 비주얼웍스나 IBM 스몰토크보다는 비주얼 스몰토크에서 현저하게 느리다. 두 객체를 하나의 참조와 바꾸기 위해 become:을 사용한 검사에서 우리는 비주얼 스몰토크에서 become: 를 실행 시 나머지 두 방언에서 사용할 때보다 100배의 시간이 소요되었다. 검사를 실행한 기계에서 각 become: 메소드에 소요된 총 시간은 수 억 초에 달했다. 비주얼 스몰토크를 사용하고 성능을 고려한다면 become:의 사용을 피하도록 한다.

또한 IBM 스몰토크에서는 각 become:의 속도는 교체되는 객체에 대한 참조 수에 따라 좌우된다는 사실도 발견했다. 참조의 수가 적은 객체에서 효율성이 좋겠지만 수 천개의 참조를 가진 객체에서 become:을 실행하는 데에도 몇 초면 충분하다.

Proxy 유지하기

Proxy를 제거하기 위한 become:의 사용은 매력적으로 보일지도 모른다. 하지만 Proxy를 클라이언트와 RealSubject 사이에 유지 시 이점은 Proxy의 RealSubject가 더 이상 필요한지를 Proxy가 결정하고 release하며, 쓰레기로 수집되도록 내버려 둔다는 점이다. 이는 메모리가 제약된 시스템에서 매우 유용한 특징이다.

예제 코드

스몰토크의 표준 Virtual Proxy의 구현을 살펴본 후 프록시 스와핑을 살펴보겠다.

표준 구조

비주얼웍스에 구현되는 Virtual Proxy의 간단한 예를 들어보겠다. 이는 [디자인 패턴] 편에서 언급한 C++에 사용된 예제 코드와 동등하다 (DP 213-215).

VisualComponent subclass: #ImageProxy
	instanceVariableNames: 'image fileName extent '
	classVariableNames: ''
	poolDictionaries: ''

비주얼웍스의 displayOn: 메소드는 Draw () 멤버 함수에 해당한다 (DP 208). Proxy가 아직 그 이미지를 참조하지 않는다면 그것을 로드하라; 그렇지 않을 시 메시지를 그 이미지로 전달할 것이다.

ImageProxy>>displayOn: aGraphicsContext
	"Display the receiver on aGraphicsContext. This proxy
	forwards display requests to its image."

	image == nil
		ifTrue: [image := self loadImage].
	^image displayOn: aGraphicsContext

loadImage 메소드는 Load () 멤버 함수에 상응한다 (DP 215):

ImageProxy>>loadImage
	| newImage |
	newImage := CachedImage on:
		(ImageReader fromFile: fileName) image.
	^newImage

마지막으로, 비주얼웍스의 preferredBounds는 GetExtent() 멤버 함수에 해당한다. 이 메시지는 RealSubject의 상의 없이 응답된다:

ImageProxy>>preferredBounds
	^0@0 extent: self extent

프록시 스와핑하기

이미지 Proxy의 스와핑 버전은 간단한 접근법을 제공한다. 이 Proxy는 doesNotUnderstand:를 사용해 주제대상으로 전송된 첫 메시지를 포착한다. 그 후에 Proxy가 주제대상이 “되어” 차후 모든 메시지들은 주제대상으로 직접 전송된다. 이러한 구현은 다음과 같은 모습일 것이다:

nil subclass: #ImageDNUProxy
	instanceVariableNames: 'fileName '
	classVariableNames: ''
	poolDictionaries: ''

ImageDNUProxy는 그 RealSubject를 가리키기 위해 굳이 image 인스턴스 변수가 필요하지 않음을 주목하라. 이는 주제대상을 가리킬 필요 없이 Proxy가 주제대상이 되기 때문이다:

ImageDNUProxy>>doesNotUnderstand: aMessage
	"This demonstrates the use of a Smart Pointer proxy.
	This object will create its RealSubject and then
	immediately swap itself out for that RealSubject."
	| image |
		(ImageReader fromFile: fileName) image.
	self become: image.	"here, self is the Proxy."
	^self			"self now refers to the image."
		perform: aMessage selector
		withArguments: aMessage arguments

앞에서 논하였듯, 프록시 스와핑을 실행하려면 =, hash, class, become: 의 구현은 Object에 발견된 것으로부터 복사할 필요가 있다.

알려진 스몰토크 사용예

VisualWorks ExternalLibraryHolder

전형적인 Virtual Proxy는 비주얼웍스 클래스 ExternalLibraryHolder에서 발견할 수 있는데, 이 클래스는 하위시스템의 일부로서 다른 언어로 쓰인 연계 라이브러리를 처리한다. ExternalInterface에게 (이 하위시스템의 Facade) 특정 라이브러리 파일의 집합을 로드하라고 알리면 즉시 로드하여 그 라이브러리로 연계하지 않는다; 대신 각 파일을 대신해 ExternalLibraryHolder를 생성시킨다.

ExternalLibraryHolder는 ExternalLibrary 객체의 로딩을 외부 호출이 발생하는 시점으로 연기한다. 좀 더 구체적으로 말해, ExternalMethod 객체가 mapAddress:를 사용해 외부 호의 주소를 매핑해야 할 때 라이브러리가 로드될 것이다. 그 후 mapAddress:로 보내지는 호는 ExternalLibraryHolder에 캐시 저장된 ExternalLibrary 로 전송될 것이다. 따라서 사용된 라이브러리만 이미지로 연계된다.

비주얼웍스의 RemoteString

비주얼웍스에서도 RemoteString 클래스의 인스턴스는 영속/가상 프록시의 역할을 한다. 기본 비주얼웍스 이미지에서 RemoteString은 클래스 주석이나 파일 내에서 실행 가능한 텍스트 조각에 대해 파일 참조를 유지한다 (소스나 변경 파일과 같은). 여기서 Proxy의 사용을 보장한다는 문제는 코드가 추가 또는 재작성됨에 따라 파일 내 텍스트 자체의 위치가 변경될지 모른다는 데에 있다. 클라이언트는 잠재적으로 부정확한 소스 텍스트 조각에 의지하기보다는 소스 또는 변경 파일에서 가장 최근 위치를 검색함으로써 필요 시 소스를 생성할 수 있는 RemoteString에 의지하는 편이 낫다.

IBM 스몰토크의 분산 프록시

IBM 스몰토크 분산 옵션에서 DsRemoteObjectPointer의 인스턴스들은 원격 객체 공간에 있는 객체를 나타낸다. DsRemoteObjectPointer는 doesNotUnderstand: 메시지를 이용해 원격 스몰토크 이미지에 있는 객체로 메시지를 전달한다. 특정 메시지들은 (become:, isKindOf:, isNil 등) 전체 네트워크 트래픽을 줄이고 두 객체 공간의 (로컬과 분산) 무결성을 확보하기 위해 부분적으로 처리된다.

비주얼웍스의 ObjectLens 프록시

비주얼웍스는 프록시를 그것의 ObjectLens 데이터베이스 시스템에 구현하는 데 있어 다양한 스와핑을 설명하는 프록시 스와핑 유형들 중 두 가지 유형을 사용한다.

Lens는 Object와 구분된 루트 계층구조를 형성하는 추상 클래스 LensAbsentee에 루트를 둔다. 클래스 LensAbsentee는 최소 Proxy 프로토콜을 구현한다. LensAbsentee는 단일 서브클래스 LensAbstractProxy를 갖고 있으며, 이것은 두 개의 구체적 서브클래스인 LensProxy와 LensCollectionProxy에서 공통 메소드를 구현한다. 두 클래스 모두 주제대상을 인스턴스화시키기 위해 doesNotUnderstand:를 재구현하며, 둘 다 원본 메시지를 전달하기 전에 주제대상이 된다 (become:).

관련 패턴

Proxy 패턴 대 Decorator 패턴

이 두 패턴은 비슷해 보이지만 다르게 행동한다. Decorator (161)와 Proxy 모두 주제대상과 동일한 타입이어서 동일한 인터페이스를 가지며, 클라이언트는 주제대상 대신 이들을 투명하게 이용할 수 있다. 둘 다 주로 주제대상으로 메시지들을 전달함으로써 자신의 인터페이스를 구현한다.

주요 차이점은, Decorator는 중첩이 가능하지만 Proxy는 중첩시킬 이유가 없다. 각 Decorator는 중첩될 때 행위를 추가한다. 하나의 Proxy가 객체의 로딩을 미루면 다른 Proxy는 쓸모가 없다. 다수의 분산/원격 프록시들을 중첩시켜 한 객체 공간의 메시지를 다른 공간, 또 다른 공간으로 반동시켜 보낼 수 있지만 비효율적으로 보인다.

Proxy 패턴 대 Template Method 패턴

Proxy처럼 (그리고 Decorator 처럼) “전달하는” 패턴에서 한 가지 문제점은 추상 클래스가 구현하는 메시지가 많을수록 전달하는 서브클래스가 전달을 위해 구현해야 하는 메시지가 많아진다는 점이다. 부분적 해결책은 Template Method (355) 패턴을 슈퍼클래스에서 공격적으로 사용하는 방법이 있다. 그리고 나면 서브클래스가 구현에 필요로 하는 유일한 메소드는 기본 연산만이 남는다. 이런 방식으로 전달하는 서브클래스는 슈퍼클래스에 정의된 모든 메시지를 전달할 필요 없이 기본 메시지만 전달하면 된다.

Notes

  1. 사실상 비주얼 스몰토크에서는 doesNotUnderstand: 를 이해하지 못하는 객체로 메시지를 전송 시 가상 머신이 사실을 알려주는 오류 대화창이 나타나지만, 이로 인해 이미지의 저장 여부를 당신에게 알리지 않은 채 스몰토크를 닫는다.
  2. Woolf(1996) 그리고 Beck(1997)의 메서드 동질성과 메서드 해싱을 보면 2개의 메소드로 구현한 추가내용이 있다