닷넷 프록시 메
쏘드 호출을 가로채기 위해서는 가로채기를 수행하는 주체가 있어야 할 것이다. 과연 누가 메쏘드 호출을 가로채는가? 이에 대한
답은 바로 프록시(proxy)이다. 프록시의 사전적인 의미는 대행, 대행자 정도로 볼 수 있다. 속어로서 프락치 어쩌구 하는
것이 바로 이 프록시다. 프록시는 구체적인 대상인 것처럼 가장하는 것이다. 가로채기의 관점에서 보면 어떤 객체의 메쏘드를
호출한다고 했을 때 프록시가 그 객체인 것처럼 사기(?)를 치고 자신이 메쏘드 호출을 받는다는 얘기가 된다. 메쏘드 호출을
가로챘으니 이제 필요한 전처리를 수행할 수도 있고 실제 메쏘드를 호출하거나, 심지어는 실제 메쏘드 호출을 하지 않고 가짜 결과를
만들어 반환할 수도 있다.
닷넷 환경에서 프록시는 다양한 분야에서 사용되고 있다. 대표적인 예로는 닷넷 리모팅을 들
수 있다. 클라이언트가 원격 애플리케이션 도메인(Application Domain; AppDomain)에 존재하는 객체를
생성하면 클라이언트는 실제 객체의 참조를 갖는 것이 아니라 리모팅 프록시에 대한 참조를 받게 된다. 그리고 이 프록시를
액세스하면 프록시는 메쏘드 호출을 메시지화해 원격 객체에 전달하게 된다. 이 외에도 COM+ 컴포넌트를 닷넷에서 개발하게 되면
Serviced Component 클래스를 사용하게 되는데, 이 클래스를 사용할 때에도
ServicedComponentProxy라고 하는 프록시가 사용된다. 이 프록시는 Managed 코드와 Unmanaged 코드를
넘나들며 COM+ 컴포넌트를 액세스하는 데 사용된다.
Real Proxy 닷넷의 프록시는
COM+의 그것과는 달리 사용자 정의가 가능하다. 즉, 독자들이 자신만의 용도로 프록시를 작성할 수 있으며, 그 프록시는
여러분의 용도에 따라 사용될 수 있다는 얘기다. 닷넷의 모든 프록시는 System.Runtime.Remoting.Proxies
네임스페이스의 RealProxy 클래스에서 파생된다. 이 클래스는 추상(abstract) 클래스로서 일반적으로 프록시에 필요한
속성과 메쏘드들이 정의되어 있으나 실제 가로챈 메쏘드 호출을 처리하는 Invoke 메쏘드는 파생 클래스에서 반드시
재정의(override)를 해야만 한다.
using System.Runtime.Remoting.Proxies; using System.Runtime.Remoting.Messaging; public abstract class RealProxy { // 다른 선언 생략 public abstract IMessage Invoke(IMessage msg); }
Invoke
메쏘드의 매개변수이자 결과 값은 IMessage 인터페이스를 구현하는 메시지 객체로서 메쏘드 호출을 메시지 형태로 변환하여
액세스할 수 있도록 해주는 객체이다. 메시지 객체와 IMessage 인터페이스에 대해서는 잠시 후에 자세히 살펴보도록 하자.
Transparent Proxy 앞
서 프록시는 RealProxy 클래스에서 파생된다고 하였다. 이름에서 풍기는 냄새로 보아 가짜 프록시도 있다는 말인가?
가짜까지는 아니지만 소위 투명한 프록시(Transparent Proxy, 이하 TP)가 존재한다. TP는 프레임워크에서 제공하는
프록시로서 메쏘드 호출을 메시지로 바꾸는 역할을 수행한다. 메쏘드 호출은 쓰레드(thread)의 스택을 통해 이뤄진다는 것쯤은
대부분의 독자들도 잘 알고(?) 있으리라 생각된다. 즉, 메쏘드를 호출할 때 필요한 매개변수들은 모두 스택에 푸시(push)되며
메쏘드 호출 이후에 결과 값이나 제어가 되돌아갈 반환 주소 역시 모두 스택에 푸시된다. 이렇게 스택에 저장된 메쏘드 호출 정보를
스택 프레임(stack frame)이라고 하며, TP는 스택 프레임의 메쏘드 호출 정보를 닷넷 메타 데이터(meta data)와
더불어 메시지화하는 역할을 수행한다.
메쏘드 호출을 메시지화하는 작업은 모든 프록시에 있어서 공통적으로 필요한
작업이며 각 프록시마다 동일한 코드가 사용될 가능성이 매우 높다. 만약 여러분이 스택 프레임을 뒤져서 메쏘드 호출을 메시지화하는
코드를 작성해야 한다면 아마도 눈앞이 캄캄해지고 눈알이 튀어나오려고 할 것이다. 다행스럽게도 닷넷 프레임워크는 TP에 대한
구현을 제공한다. 따라서 커스텀 프록시를 작성하고자 한다면 단순히 TP를 생성하기만 하면 된다.
RealProxy.GetTranspareProxy 메쏘드는 생성된 TP 인스턴스의 레퍼런스를 반환한다. TP는 그 특성상
프록시가 사용되는 특정 타입(클래스)에 일대일로 대응된다. 예를 들어 어떤 타입 T1, T2, T3이 프록시를 사용한다면 각각의
타입에 대한 TP가 존재한다는 말이다. 이 때문에 TP는 자신이 대리하고 있는 타입 정보를 잘 알고 있다. TP는 거의 완벽하게
대상 타입을 흉내낸다. 당연하게 TP를 생성하는 RP(Real Proxy)는 프록시 대상 타입에 대한 타입 정보(Type 클래스)를 필요로 하게 된다.
완
전한 코드는 아니지만 <리스트 3>의 코드를 보자. MyProxy는 RealProxy에서 파생됐다. 그리고 그
생성자(constructor)에서 프록시 대상이 되는 TargetClass 타입에 대한 정보를 베이스 클래스로 넘겨주고 있음을
주의하자. 이렇게 타입 정보가 있어야만 TP를 생성할 수 있다. 또한, TP를 TargetClass 타입으로 형 변환하는 것
역시 유심히 살펴볼 필요가 있다.
<리스트 3> 프록시 클래스 및 사용 예제 class TargetClass : MarshalByRefObject { public void DoOperation() { } }
class MyProxy : RealProxy { // 베이스 클래스에 프록시 대상이 되는 클래스의 타입을 알려 주어야만 TP를 생성할 수 있다. public MyProxy(Type type) : base(type) { } // 생략 }
// 프록시 사용 예제 RealProxy rp = new MyProxy(typeof(TargetClass));TargetClass obj = (TargetClass)rp.GetTransparetProxy(); obj.DoOperation() Debug.Assert(obj is TargetClass);
프록시 대상 타입 닷
넷의 모든 타입에 대해 프록시를 사용할 수 있는 것은 아니다. CLR은 MarshalByRefObject 클래스에서 파생된
타입에 대해서만 프록시를 사용할 수 있다. 따라서 MarshalByRefObject에서 직접/간접적으로 파생되지 않은 클래스나
값 타입(value type)에 대해서는 프록시를 사용할 수 없다.
반드시 알아야 하는 것은 아니지만
MarshalByRefObject가 프록시를 사용할 수 있는 타입이 되는 이유를 설명하자면 이렇다. CLR은 필요에 따라
메쏘드를 인라인(inline)화한다. C++에서는 명시적으로 inline 키워드를 사용하지만 CLR은 상황에 따라 작은 코드를
갖는 메쏘드를 인라인 메쏘드로 호출자의 코드에 직접 메쏘드 코드를 삽입하기도 한다. 그런데 이 인라인 코드 생성은 프록시가
메쏘드 호출을 가로챌 수 없게 만드는 요인이 되어 버린다. 반면, MarshalBy RefObject 클래스에서 직/간접적으로
파생된 클래스에 대해 CLR은 전혀 인라인 코드 생성을 수행하지 않는다. 즉, MarshalBy RefObject에서 파생된
클래스의 인스턴스에 대해 메쏘드 호출을 수행할 때는 이 메쏘드 호출이 결코 인라인화되지 않는다고 장담할 수 있는 것이다.
프
록시가 메쏘드 호출을 가로채기 위해서는 메쏘드 호출이 인라인화되지 않을 것을 요구한다. 따라서 프록시가 사용될 수 있는 타입이
MarshalByRefObject인 것이다. 이런 이유로 Don Box는 자신의 저서에서 이 클래스의 이름이 그 의미와는 다르게
지어졌다는 것을 지적하고 있다. 사실 MarshalByRefObject에서 파생된 클래스가 반드시 참조에 의해 마샬링되는 것은
아니다. 어찌 되었건 프록시가 사용될 수 있는 클래스는 MarshalByRefObject에서 직/간접적으로 파생된 클래스이어야
한다는 사실만을 잊지 말자. 이로서 <리스트 3>에서 TargetClass 타입이 왜
MarshalByRefObject 클래스에서 파생되었는가에 대한 답이 될 것이다.