SSISO Community

갤러리정

AOP를 향하여, 닷넷 프록시 - 4

AOP를 향하여, 닷넷 프록시 - 4
  저 자 : 유경상
  출판일 : 2003년 7월호

  예제 프록시 구현
이 제 지루한 이론은 던져 버리고 실제 커스텀 프록시를 구현해 보자. 우리가 구현해 볼 첫 번째 프록시는 매우 간단한 것으로 단순히 메쏘드 호출 전/후에 프록시가 사용되고 있음을 알리는 메시지를 콘솔에 출력하는 정도로만 구현해 볼 것이다.

커스텀 프록시 구현
커 스텀 프록시로서 MyProxy를 구현해 보자. 이 프록시의 구현은 <리스트 4>와 같다. MyProxy의 생성자의 매개변수로 프록시의 대상이 되는 객체를 취하는 것에 주목하자. 프록시의 대상이 필요한 이유는 ExecuteMessage 메쏘드 호출에 대상 객체를 필요로 하기 때문이다. 또한 MyProxy의 베이스 클래스인 RealProxy 클래스에 대상 타입에 대한 타입 정보를 알려주기 위해 RealProxy의 생성자를 호출하고 있음에도 유의하자.

<리스트 4> 간단한 프록시 구현
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Proxies;
using System.Runtime.Remoting.Messaging;

class MyProxy : RealProxy
{
private MarshalByRefObject m_Target;

public MyProxy(MarshalByRefObject target) : base(target.GetType())
{
m_Target = target;
}

public override IMessage Invoke(IMessage msg)
{
Console.WriteLine("Some pre-processing...");
IMethodCallMessage callMsg = (IMethodCallMessage)msg;
IMessage retMsg = RemotingServices.ExecuteMessage(m_Target, callMsg);
Console.WriteLine("Some post-processing...");
return retMsg;
}
}

MyProxy 의 가장 핵심적인 구현은 Invoke() 메쏘드의 구현이다. 이 메쏘드는 앞서 설명한 대로 매개변수로 받은 메시지 객체를 ExecuteMessage에게 넘겨주어 실제 메시지 호출이 이뤄지도록 하고 있다. 또한 메시지 출력이라는 간단한 전처리/후처리 역시 수행하고 있음을 주의깊게 살펴보자.

이 프록시를 사용하는 코드를 살펴보도록 하자. 앞서 설명처럼 프록시의 대상이 되는 TargetObject 클래스는 MarshalByRefObject 클래스에서 파생됐다. MyProxy가 매우 간단하지만 MarshalBy RefObject에서 직/간접적으로 파생되는 모든 클래스에 대해 사용될 수 있다는 점에서는 매우 유연하다고 할 수 있다. Main() 내부에서는 명시적으로 MyProxy의 인스턴스를 생성하고 GetTransparent Proxy를 호출하여 TargetObject 클래스에 대한 TP를 생성한다. 비록 GetTransparentProxy 메쏘드가 반환한 객체가 TP이지만 이 객체는 마치 TargetObject인 것처럼 사용할 수 있다. <리스트 5>에 대한 수행 결과는 다음과 같다.

Some pre-processing...
foo() invoked !!!
Some post-processing...

<리스트 5> MyProxy를 사용하는 코드 예제
class TargetObject : MarshalByRefObject
{
public void foo()
{
Console.WriteLine("foo() invoked !!!");
}
}

class Test
{
public static void Main()
{
MyProxy proxy = new MyProxy(new TargetObject());
TargetObject obj = (TargetObject)proxy.GetTransparentProxy();
obj.foo();
}
}

< 리스트 5>는 간단하지만 프록시를 프로그래머가 명시적으로 생성하는 코드는 그다지 좋은 모습이라고 할 수 없다. 먹지도 못할 패턴(pattern), 배운 것 두었다 뭐에 쓰겠는가? 본지에서도 여러 번 다루었던 디자인 패턴을 적용하면 더욱 우아한 코드를 작성할 수 있다. 팩토리 메쏘드 패턴을 TargetObject 클래스에 적용하면 있어 보이는 코드를 작성할 수 있다. <리스트 6>은 <리스트 5>에 패턴을 적용한 것이다. TargetObject의 생성자를 private로 바꿈으로써 외부 코드는 new를 이용하여 직접적으로 TargetObject의 인스턴스를 생성할 수 없다. 대신 스태틱 메쏘드인 CreateInstance()를 호출해야 하는데, CreateInstance 메쏘드는 슬쩍 TP를 반환하여 프록시가 사용되도록 강제할 수 있다.

<리스트 6> 팩토리 메쏘드 패턴을 적용한 예제
class TargetObject : MarshalByRefObject
{
private TargetObject() { }
public static TargetObject CreateInstance()
{
MyProxy proxy = new MyProxy(new TargetObject());
return (TargetObject)proxy.GetTransparentProxy();
}
public void foo()
{
Console.WriteLine("foo() invoked !!!");
}
}

class Test
{
public static void Main()
{
TargetObject obj = TargetObject.CreateInstance();
obj.foo();
}
}

어드밴스드 프록시 프로그래밍
지 금까지 닷넷에서 사용되는 메쏘드 가로채기의 원리와 프록시의 종류, 그리고 메쏘드 메시지에 대한 것을 살펴봤으며 간단한 커스텀 프록시 역시 작성해 보았다. 프록시를 응용할 수 있는 분야는 대단히 많으며, 독자들이 느끼지 못하는 사이에도 프록시는 사용되고 있다. COM+ 컴포넌트를 작성할 때 우리는 ServicedComponent 클래스를 사용한다. ServicedComponent에서 파생된 클래스를 사용할 때마다 우리는 ServiedComponentProxy라는 프록시를 반드시 사용하고 있는 것이다.

그런데 이상한 것은 ServicedComponent에서 직/간접적으로 파생된 클래스의 인스턴스를 생성할 때 단순히 new 연산자를 사용했을 뿐이지 <리스트 5>나 <리스트 6>과 같은 코드를 사용한 적이 없을 것이다. 또 알지 못하는 다른 것이 숨어 있을까?

그렇다. 앞서 구현한 MyProxy는 대상 객체의 생성 과정을 가로채지 못한다. 너무도 자명한 것이 프록시가 생성 과정에 참여하지 않고 다른 코드에 의해 명시적으로 new를 통해 대상 객체를 생성해 버리기 때문이다. 따라서 Invoke 메쏘드는 결코 IConstructionCall Message 객체를 매개변수로 받을 수 없다는 것이다. 프록시가 객체의 생성 과정을 가로채기 위해서는 특별한 방법을 필요로 한다.

ProxyAttribute
프록시가 객체 생성 과정을 가로채기 위해서는 프록시 특성(proxy attribute)을 필요로 한다. 프록시 특성은 일반적인 커스텀 특성과는 다르게 CLR에 의해 특별하게 취급받는다. 일반적인 커스텀 특성은 프로그래머가 직접 이 커스텀 특성을 메타 데이터로부터 읽고 적절한 처리를 수행해 주지만 프록시 특성은 CLR에 의해 검출되며 프록시 특성이 추가된 클래스는 new 연산자가 호출될 때 일반적인 객체 생성 과정을 따르지 않고 객체 생성을 프록시 특성에 위임한다. 프록시 특성은 실제 객체 대신 프록시를 생성하고 반환할 수 있다. 그리고 이를 통해 생성자 호출 또한 프록시가 가로챌 수 있게 된다.

프록시 특성은 ProxyAttribute에서 파생된 특성 클래스이다. CLR이 ProxyAttribute에서 직/간접적으로 파생된 특성이 정의된 클래스의 인스턴스를 생성하게 되면 객체 생성을 ProxyAttribute 클래스의 CreateInstance() 메쏘드에 위임한다. 즉, new 연산자의 결과로서 ProxyAttribute.CreateInstace() 메쏘드의 결과를 사용한다는 말이다. 따라서 CreateInstance 메쏘드가 실제 객체가 아닌 TP를 반환한다면 <리스트 5>나 <리스트 6>과 같은 이상한(?) 객체 생성 코드를 사용할 필요가 없게 된다. <리스트 7>은 간단한 Create Instance 메쏘드의 구현을 보여준다.

<리스트 7> ProxyAttribute 구현
[AttributeUsage(AttributeTargets.Class)]
class MyProxyAttribute : ProxyAttribute
{
public override MarshalByRefObject CreateInstance(Type serverType)
{
MarshalByRefObject target = base.CreateInstance(serverType);
MyProxy proxy = new MyProxy(target, serverType);
MarshalByRefObject obj = (MarshalByRefObject)proxy.GetTransparentProxy();
return obj;
}
}

< 리스트 7>의 CreateInstance 메쏘드의 구현은 잘 이해되지 않는 부분이 있다. 먼저 MyProxy를 생성하는 과정에서 <리스트 4>와는 다르게 실제 객체의 참조와 타입을 모두 생성자의 매개변수로 넘겨준다는 점이다. <리스트 4>의 생성자 구현은 실제 객체의 GetType() 메쏘드를 호출함으로서 타입을 알아낼 수 있다. 이것이 가능한 이유는 실제 객체가 이미 생성되었기 때문이다. 하지만 ProxyAttribute의 CreateInstance 메쏘드는 실제 객체가 아직 생성되지 않은 상태이기 때문에 객체의 GetType() 메쏘드 호출이 예외를 발생시킨다. 따라서 명시적으로 실제 객체의 타입을 매개변수로 넘겨주는 것이다. 더욱이 CLR이 CreateInstance 메쏘드를 호출할 때 생성할 객체의 타입을 매개변수(serverType)로 넘겨주므로 아무런 문제가 없다.

CreateInstace 메쏘드가 실제 객체가 아닌 TP를 반환하는 것에 주목할 필요가 있다. 이것은 <리스트 6>의 팩토리 메쏘드가 수행하는 것과 비슷한 효과를 낸다. 하지만 반드시 주의해야 할 것이 있다. ProxyAttribute.CreateInstance 메쏘드의 수행 시점은 객체가 생성됐고 아직 생성자가 수행되지 않은 상황이라는 점이다. MSDN 라이브러리의 설명을 그대로 옮기자면 CreateInstance 메쏘드는 TP를 반환하거나 초기화되지 않은, 즉 메모리 할당만 이뤄지고 생성자가 아직 호출되지 않은 객체를 반환해야 한다. 약간 복잡하지만 ProxyAttribute.CreateInstance()가 클라이언트 측에서 수행될 때는 TP를 반환하며 서버측에서 수행될 때는 초기화되지 않은 서버 객체를 반환한다. 클라이언트측과 서버측을 나누는 구분은 닷넷 리모팅에서는 명확하지만, 지금까지 다룬 예는 클라이언트와 서버가 동일한 애플리케이션 도메인 내에 있으므로 base.CreateInstance()를 호출함으로써 CLR의 기본 디폴트 프록시(RemotingProxy 클래스)를 반환받을 수 있다.

하지만 아직 생성자는 호출되지 않은 상태이다. 따라서 CreateInstance() 메쏘드 종료 이후에 CLR은 생성자 호출을 수행할 것이며, 이 생성자 호출은 프록시에 의해 가로채이게 된다.

930 view

4.0 stars