DesignPatternSmalltalkCompanion:Proxy
PROXY (DP 207)
의도
다른 객체로의 접근을 통제하기 위해서 다른 객체의 대리자 또는 자리메꿈을 제공한다.
구조
논의
객체지향 디자인에서 중요한 부분은 어떠한 객체가 다른 어떤 객체를 아는지 결정하는 것이다. 스몰토크에서 "앎의 연결"은 주로 객체로의 직접 참조 또는 Observer 패턴과 같은 일반적 메커니즘을 통해 이루어진다. 하지만, 때로는 다른 실제적 객체를 아예 참조하고 싶지 않을 때가 있다. 대신 "이러이러한 종류의 객체가 여기에 있어야 한다." 라고 말하고 실제 객체의 대리자를 참조하는 경우가 있다. 이러한 바람이 바로 Proxy 패턴의 동기이다.
Proxy 패턴에서 핵심은 동일한 타입의 두 개의 객체, 즉, 자리메꿈객체와 실제 객체에 있는데, 여기서 자리메꿈객체는 실제 객체로의 접근을 통제한다. 자리메꿈(Proxy)은 완전히 보호된 객체 RealSubject와 같은 인터페이스를 가지므로 나머지 시스템은 RealSubject를 사용하고 있다고 생각하지만 사실은 Proxy를 사용하고 있는 것이다.
Proxy는 그것의 제한된 상태와 행위(behavior)를 이용해 가능한 한 많은 메시지를 처리하고, 어쩔 수 없는 경우에만 RealSubject로 메시지를 전달한다. 이는 Proxy가 시스템의 RealSubject 사용을 제한시키거나 최적화시킬 수 있도록 해준다.
스몰토크에서 사용되는 Proxy 패턴
Proxy는 스몰토크에서 흔히 사용되는 패턴으로, 주로 다음의 두 가지 유형이 사용된다:
- 영속적 프록시. 관계형 데이터베이스나 객체 데이터베이스 또는 평면적인 파일과 같은 영속적 저장소에서 객체를 로딩하는 것은 시간이 많이 소요되는 경우가 많다. 수 많은 영속 객체를 로딩하는 것은 시간이 많이 소요될 뿐 아니라 낭비이다. 특히 그 객체가 전혀 사용되지 않는다면 더욱 그렇다. 영속적 객체를 위한 프록시는 전혀 사용되지 않는 객체의 로딩을 피하게 해준다. 거의 모든 상업적 데이터베이스 프레임워크가 이러한 형태의 Proxy 패턴을 최소한 하나씩은 포함한다. 객체 데이터데이터 베이스 업자들이 스몰토크 언어에 바인딩할 때에도 이를 많이 사용한다. 이것을 [디자인 패턴] 용어로 가상 프록시라고 한다.
- 분산 프록시. 논리적 스몰토크 이미지가 물리적 이미지들에 걸쳐 분산될 때 각 객체는 하나의 물리적 이미지에 존재할 수 있지만 다른 이미지들로부터 접근할 수 있어야 한다. 물리적 이미지들에 걸친 메시지 송신은 이미지 내의 메시지 송신보다 느리다. 원격 객체로의 프록시는 메시지 송신을 최소화시킴으로써 협력을 최적화시킬 수 있고, 이에 따라 원격 객체가 로컬 객체에 가깝게 행동하도록 만들 수 있다. 시장에 나와 있는 분산 스몰토크, VisualAge Distributed Option과 ParcPlaceDigitalk Distributed Smalltalk 모두 이 패턴을 사용한다. [디자인 패턴]에서는 이를 원격 프록시라 일컫는다 (DP 208).
[디자인 패턴] 편에서는 두 가지 유형의 프록시(보호용 프록시와 지능적 참조)를 더 언급한다. 하지만 이러한 프록시들은 스몰토크에서는 다른 언어에서만큼 흔히 사용되지 않는다. 이 책에서는 앞서 언급하였던 두 가지 유형의 프록시를 중점적으로 살펴보고자 한다.
구현
(거의) 모든 것을 전달하는 프록시
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 등등 같이) 명시적으로 재구현 하고 싶지 않은 메서드 마저도 상속한다는 것을 의미한다.
이 문제점의 근본적인 원인은 Proxy가 Object의 서브클래스라는 것에 있기 때문에 이것를 해결할 한 가지 방법으 이 관계를 없애는 것이겠다. 어떻게 하면 될까? Proxy를 nil로부터 상속하기. 그렇다, 스몰토크에서는 이것이 가능하다. Object의 슈퍼클래스는 무엇인가? 바로 nil이다. nil이 하나의 서브클래스를 가질 수 있다면 다른 서브클래스들을 가질 수도 있는 것이다. 하지만, 여기에는 치뤄야 할 댓가가 있다.
스몰토크의 간단한 일관성들 중 하나는 모든 것이 객체라는 점이다. 이는 모든 것이 메시지를 수신할 수 있는 추상 데이터 타입임을 의미할 뿐 아니라 모든 것은 한 클래스의 인스턴스이고 모든 클래스는 Object의 서브클래스이다 (Object 자체만 제외하고). Proxy에 대한 이 접근법은 이 단순한 진실을 깬다. Proxy들은 여전히 객체이지만 Object의 일종은 아니다.
전달해선 안 되는 메시지
일반적으로 클래스를 nil의 서브클래스로 정의하는 것은 스몰토크의 객체들에 대한 많은 기본적 가정을 깨트리기 때문에 위험하다. 예를 들어, Visual Smalltalk에서는 미묘한 버그로 인해 새로운 브라우저를 열기 전까지는 Class Hierarchy Brower에 있는 nil에 대해 새로 정의된 서브클래스를 볼 수 조차 없다. nil의 서브클래스로서 Proxy가 이해하는 유일한 메시지는 ==(double-equal, 이중 등호)인데, 그 이유는 그것이 메시지로서 전송되는 것이 아니라 가상 머신에 의해 직접 구현되기 때문이다. Proxy는 doesNotUnderstand: 를 구현해야 하며, 그렇지 않으면 그것이 구현하지 않는 메시지는 모두 무한 루프를 야기한다.[1] 이중 등호를 제외한 모든 메시지에 대해서, Proxy는 메시지를 스스로 구현하거나 doesNotUnderstand: 에 의해 RealSubject로 메시지가 전달되도록 하거나를 선택할 수 있다.
Proxy가 구현할 필요가 있을 것으로 예상되는 다른 메서드들을 소개하겠다:
- isProxyㅡ이것은 객체가 프록시인지 아닌지를 말해줄 수 있는 간단한 방법을 제공한다. inspect를 사용하거나 printString에 의존해선 안 되는데 그 이유는 두 메서드 모두 주제대상으로 전송될 것이기 때문이다. isProxy는 두 군데에 구현해야 한다. Proxy는 true를, Subject는 false를 반환하도록 한다.
- become:ㅡ이것은 설계에서 Proxy 대신 RealSubject로 교체하고 싶을 때 구현해야 한다 (Proxy를 위한 RealSubject의 스와핑은 아래를 참조).
- =과 hashㅡProxy를 비교하거나 Set 또는 다른 Collection에 저장할 경우 이 메시지들을 구현한다.[2]
- halt와 (Visual Smalltalk의) vmInterrupt: ㅡ프록시를 디버깅할 계획이 있다면 이 메시지들을 구현한다.
- classㅡProxy가 그것의 클래스쪽 메서드로 접근할 필요가 있다면 이 메시지를 구현한다.
이 메시지들을 구현하려면 Object의 구현으로부터 코드를 복사한다.
RealSubject와 그 프록시의 스와핑
Proxy의 결점은 그들이 발생시킨 오버헤드로서, 특히 성능을 향상시킬 목적으로 프록시를 사용하고 있다면 그 단점이 더 부각된다. 예를 들어, 영속적 Proxy는 처음에는 진짜로 필요하기 전까지는 영속 객체의 로딩을 피하기 때문에 오버헤드를 감소시킨다. 하지만 영속 객체가 로드되는 순간부터 Proxy는 방해가 되기 시작한다. 메시지 전송이 두 배가 된다고 해도 성능 저하는 작다. Proxy는 아마도 그다지 많은 메모리를 차지하진 않을 것이다. 하지만 일부 애플리케이션에서 오버헤드가 상당할 수 있으며, 설계자는 RealSubject를 이용할 수 있는 순간부터 Proxy를 제거하고 싶을 것이다.
이러한 프록시의 제거는 become: 이라 불리는 메시지를 사용해 달성할 수 있다. become:이 하는 일은 수신자를 향한 모든 포인터를 argument 객체를 향하도록 변경하는 것이다. 이는 한방향 become: 으로서 Visual Smalltalk에서 사용된다. become: 메서드의 실행 후, 수신자를 참조하던 모든 객체들은 이제 argument를 가리키게 된다. VisualWorks와 IBM Smalltalk에서는 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를 갖고 있고 Proxy를 제거하고 RealSubject를 직접 사용하고자 한다면, become: 으로 바꿔치기 할 수 있다.
aProxy become: aRealSubject
Proxy의 모든 클라이언트들은 이제 RealSubject를 참조한다. become: 이 한 방향이라면 Proxy는 여전히 RealSubject를 참조할 것이다. become: 이 양방향이라면 Proxy의 RealSubject 를 가리키던 변수가 이제 스스로를 가리키고 있을 것이다! 어떤 경우이건, become:을 하기 전에, RealSubject를 참조하는 것은 Proxy뿐이었다면, 이제 어떠한 것도 Proxy를 참조하지 않으므로 쓰레기로서 수집될 것이다.
doesNotUnderstand:와 become: 의 시사점
doesNotUnderstand:와 become: 좋은 쪽과 나쁜쪽의 큰 영향을 함께 준다. Beck (1997)이 언급하였듯 become: 은 설계를 이해하기 힘들게 만들고 코드의 디버깅을 까다롭게 만들기 때문에 일반적으로 피해야 한다. doesNotUnderstand:의 경우도 마찬가지다. 게다가 이 메시지들은 빠른 메시지가 아니기 때문에 성능 저하를 일으킨다.
Proxy에서 doesNotUnderstand:를 사용할 경우, 두 가지 이점이 있다:
- 이것은 Proxy 내의 코드를 단순화시켜 작성할 코드가 줄어든다.
- 이것은 유연하여 인터페이스 변경을 알아서 처리한다.
여기에는 두 가지 단점도 있다:
- 디버깅하기가 힘들다. 객체들이 구현하지 않은 메시지가 처리되는 것을 보는 것은 쉽지 않다.
- 직접 메시지 송신보다 느리다.
doesNotUnderstand:의 힘에 도취된 프로그래머들이 이를 남용하여 지나치게 일반화되거나 비효율적인 결과를 낳게 되었다. 스몰토크는 메시지 송신에 최적화되어 있다는 사실을 기억하고, 가급적 메시지 송신을 사용하도록 하라. 일반 메시지 송신 대신 doesNotUnderstand: 를 사용 시 성능적 측면에서 상당한 불이익도 발생하기도 한다. Visual Smalltalk에서는 그 차이가 작지만, doesNotUnderstand:는 IBM Smalltalk에서는 가 3배 정도 느리며, 비주얼웍스에서는 6배 가량이 느리다.
become: 메서드 또한 느리게 실행될 수 있다. become:은 VisualWorks나 IBM Smalltalk보다는 Visual Smalltalk에서 현저하게 느리다. 한 번씩만 참조되고 있는 두 객체를 맞바꾸는 become:을 실행한 검사에서 우리는 Visual Smalltalk에서 나머지 두 방언에서 보다 100배의 시간이 소요되는 것이 확인되었다. 검사를 실행한 기계에서 become: 메서드에 소요된 시간은 수백 밀리초에 달했다. Visual Smalltalk를 사용하면서 성능을 고려한다면 become:의 사용을 피하도록 한다.
또한 IBM Smalltalk에서는 become:의 속도는 교체되는 객체의 참조 수에 따라 좌우된다는 사실도 발견했다. 참조의 횟수가 적은 객체에서 효율성이 좋지만, 수 천개의 참조를 가진 객체에서 become:을 실행하면 몇 초씩 걸리기도 한다.
Proxy 유지하기
Proxy를 제거하기 위한 become:의 사용은 매력적으로 보일지도 모른다. 하지만 Proxy를 클라이언트와 RealSubject 사이에 유지할 때의 이점은 Proxy의 RealSubject가 더 이상 필요한지를 Proxy가 결정하고 release하며, 쓰레기로 수집되도록 할 수 있다 점이다. 이는 메모리가 제약이 있는 시스템에서 매우 유용한 특징이다.
예제 코드
스몰토크의 표준 Virtual Proxy의 구현을 살펴본 후 Swapping Proxy를 살펴보겠다.
표준 구조
VisualWorks에 구현된 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
Swapping Proxy
이미지 Proxy의 스와핑 버전은 간단한 접근법을 제공한다. 이 Proxy는 doesNotUnderstand:를 사용해 주제대상으로 전송된 첫 메시지를 포착한다. 그 후에 Proxy가 주제대상이 "되어(become:)" 차후 모든 메시지들은 주제대상으로 직접 전송된다. 이러한 구현은 다음과 같은 모습일 것이다:
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
앞에서 논하였듯, 스와핑 프록시가 동작하려면, Object로부터 =, hash, class, become: 의 구현을 복사할 필요가 있다.
알려진 스몰토크 사용예
VisualWorks ExternalLibraryHolder
전형적인 Virtual Proxy는 비주얼웍스의 클래스 ExternalLibraryHolder에서 발견할 수 있는데, 이 클래스는 하위시스템의 일부로서 다른 언어로 쓰인 외부 링크 라이브러리를 담당한다. ExternalInterface(이 하위시스템의 Facade)에게 특정 라이브러리 파일의 집합을 로드하라고 하면, 그것들을 즉시 로드하여 라이브러리로 링크하지 않는다; 대신 각 파일을 대신하는 ExternalLibraryHolder를 생성시킨다.
ExternalLibraryHolder는 ExternalLibrary 객체의 로딩을 외부 호출이 발생하는 시점으로 연기한다. 좀 더 구체적으로 말해, ExternalMethod 객체가 mapAddress:를 사용해 외부 호출의 주소를 매핑해야 할 때 라이브러리가 로드될 것이다. 그 후 mapAddress:로 보내지는 호출은 ExternalLibraryHolder의 캐시로 가지고 있는 ExternalLibrary 로 전송될 것이다. 따라서 사용된 라이브러리만 이미지로 링크된다.
VisualWorks의 RemoteString
VisualWorks에서도 RemoteString 클래스의 인스턴스는 영속/가상 프록시의 역할을 한다. 기본 VisualWorks 이미지에서 RemoteString은 클래스 주석이나 파일 내에서 실행 가능한 텍스트 조각에 대해 (소스나 변경 파일과 같은)파일 참조를 가진다. 여기서 Proxy의 사용을 보장하는 것은 문제가 있는데, 코드가 추가 또는 재작성됨에 따라 파일 내 텍스트 자체의 위치가 변경될지 모르기 때문이다. 클라이언트는 잠재적으로 부정확할 수 있는 소스 텍스트 조각에 의지하기보다는 필요할 때마다 소스 또는 변경 파일에서 가장 최근 위치를 접근하여 소스를 재생성할 수 있는 RemoteString에 의지한다.
IBM Smalltalk의 분산 프록시
IBM Smalltalk Distributed Option에서 DsRemoteObjectPointer의 인스턴스들은 원격 객체 공간에 있는 객체를 나타낸다. DsRemoteObjectPointer는 doesNotUnderstand: 메시지를 이용해 원격 스몰토크 이미지에 있는 객체로 메시지를 전달한다. 특정 메시지들(become:, isKindOf:, isNil 등)은 전체 네트워크 트래픽을 줄이고 두 객체 공간의 (로컬과 분산) 무결성을 확보하기 위해 지역적으로 처리된다.
VisualWorks의 ObjectLens 프록시
VisualWorks는 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) 패턴을 슈퍼클래스에서 공격적으로 사용하는 방법이 있다. 그렇게 하면, 서브클래스는 근원적인 동작만 구현하면 된다. 이런 방식으로 떠넘기는 서브클래스는 슈퍼클래스에 정의된 모든 메시지를 전달할 필요 없이 근원적인 메시지만 전달하면 된다.