SSISO Community

갤러리정

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

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

  생성자 호출 메시지 처리
ProxyAttribute 를 이용하여 객체 생성 과정까지 가로챌 수 있게 되었으므로 이제 프록시도 생성자 호출 메시지를 처리할 수 있어야 한다. 객체 생성 메시지는 IConstructionCallMessage 인터페이스를 통해 액세스할 수 있다. 즉, Invoke가 매개변수로 받는 IMessage 객체가 IConstructionCallMessage 인터페이스를 지원한다면 그 메시지는 객체 생성 메시지로 판단할 수 있다.

<리스트 8> ProxyAttribute 지원을 위해 향상된 프록시 코드
class MyProxy : RealProxy
{
private MarshalByRefObject m_Target;

public MyProxy(MarshalByRefObject target, Type serverType)
: base(serverType)
{
m_Target = target;
}

public override IMessage Invoke(IMessage msg)
{
IMessage retMsg = null;

Console.WriteLine("Some pre-processing...");
if (msg is IConstructionCallMessage) {
// 객체 생성 메시지 처리
IConstructionCallMessage ctorMsg = (IConstructionCallMessage)msg;
RealProxy proxy = RemotingServices.GetRealProxy(m_Target);
// 실제 객체를 생성한다.
proxy.InitializeServerObject(ctorMsg);
// 객체 생성 결과를 ‘'만들어’ 반환한다.
retMsg = EnterpriseServicesHelper.CreateConstructionReturnMessage
(ctorMsg, (MarshalByRefObject) this.GetTransparentProxy());
}
else {
IMethodCallMessage callMsg = (IMethodCallMessage)msg;
retMsg = RemotingServices.ExecuteMessage(m_Target, callMsg);
}
Console.WriteLine("Some post-processing...");
return retMsg;
}
}

< 리스트 8>은 생성 과정을 고려한 프록시 코드이다. 메시지 객체가 IConstructionCallMessage를 지원하는가를 검사하는 코드를 주의깊게 살펴볼 필요가 있다. <리스트 7>에서 CreateInstance() 메쏘드 구현을 살펴보면 base.CreateInstance() 호출이 실제 객체를 생성하는 것이 아니라 디폴트 프록시(System.Runtime.Remoting. Proxies.RemotingProxy 클래스의 TP)를 생성하여 반환함에 유의해야 한다. 따라서 아직 실제 객체가 생성되지 않았다. MyProxy의 m_Target 필드에 저장된 객체는 RemotingProxy의 Transparent 프록시이므로 실제 객체를 생성할 필요가 있다. 이를 위해 Remoting Proxy 클래스의 InitializeServerObject 메쏘드를 호출한다. 이 메쏘드는 생성자가 아직 호출되지 않은 객체를 힙 상에 생성하고 IConstructionCallMessage를 이용하여 생성자를 호출해 준다.

이 때 또 한 가지 주목할 사항은 실제 객체를 new를 이용하여 작성할 수 없다는 점이다. ProxyAttribute가 사용됐기 때문에 new를 사용하면 ProxyAttribute의 CreateInstance가 호출될 것이고 이는 다시 프록시의 Invoke 메쏘드에 대해 객체 생성 메시지를 발생시킨다. 즉, 무한 루프가 발생한다. 이 때문에 InitializeServerObject 메쏘드를 사용하여 실제 객체의 생성을 수행하는 것임에 주의하자.

객체를 생성하고 생성자 호출이 끝났다면 Invoke 메쏘드는 결과를 반환해야 한다. IConstructionCallMessage 객체를 매개변수로 받았기 때문에 반환은 IConstructionReturnMessage가 돼야 한다. IConstructionReturnMessage 메시지는 생성된 객체의 참조를 반환해야 한다. 여기서 반환된 값이 new 연산자의 결과 값으로 반환되기 때문이다. 혼동해서는 안될 것은 ProxyAttribute.CreateInst ance 메쏘드가 반환한 객체는 생성자 호출의 대상으로 사용되며 IConstructionCallMessage 처리의 결과로 반환되는 IConstruction ReturnMessage가 new 연산자의 결과가 된다는 점이다. 객체 생성뿐만 아니라 객체의 인스턴스 메쏘드 호출 역시 가로채야 한다면, new 연산자가 반환하는 객체 참조는 프록시의 TP이어야 할 것이다.

이 때문에 InitializeServerObject() 메쏘드가 IConstructionReturn Message를 반환함에도 불구하고 이 메시지를 그대로 사용할 수 없다. 해결 방법은 MyProxy의 TP를 반환하는 IConstructionReturn Message를 만들어 내도록 System.Runtime. Remoting.Services 네임스페이스의 EnterpriseServiceHelper 클래스의 CreateConstr uctionCall Message 메쏘드를 사용하여 MyProxy의 TP가 반환되도록 조정할 수 있다. <리스트 8>의 다음 코드가 그와 같은 역할을 수행한다.
retMsg = EnterpriseServicesHelper.CreateConstructionReturnMessage
(ctorMsg, (MarshalByRefObject) this.GetTransparentProxy());

이 렇게 메쏘드 호출 결과를 임의로 생성함으로써 마치 메쏘드가 호출된 것처럼 흉내내는 기법은 여러모로 유용할 때가 많다. 또한 메쏘드 수행 결과를 변형하는 것 역시 가능하다. <리스트 8>의 Invoke 메쏘드 구현을 다음과 같이 수정한다면 실제 메쏘드 호출은 전혀 이뤄지지 않을 것이다.

public override IMessage Invoke(IMessage msg)
{
IMessage retMsg = null;

Console.WriteLine(“Some pre-processing...”);
if (msg is IConstructionCallMessage) {
// 생략
}
else {
IMethodCallMessage callMsg = (IMethodCallMessage)msg;
retMsg = new ReturnMessage(null, null, 0, null, callMsg);
}
Console.WriteLine(“Some post-processing...”);
return retMsg;
}

프록시의 사용
이 제 프록시는 메쏘드 호출뿐만 아니라 객체 생성까지 가로채기 할 수 있는 프록시를 구현했다. AOP를 적용할 만반의 준비를 갖춘 것이다. 이제 남은 건 <리스트 7>의 MyProxyAttribute를 클래스에 다음과 같이 명시하기만 하면 된다.

[MyProxy]
class TargetObject : MarshalByRefObject { ... }

아 쉽게도 앞 코드는 작동하지 않는다. 현재 버전의 CLR은 ProxyAttribute가 MarshalByRefObject에서 파생된 클래스에 명시하는 것을 지원하지 않는다. 다만 ContextBoundObject에서 파생된 클래스에 대해서만 ProxyAttribute 를 허용하고 있다. 따라서 MyProxy를 사용하는 예제 코드는 <리스트 9>와 같게 된다.

<리스트 9> 프록시를 사용하는 예제
[MyProxy]
class TargetObject : ContextBoundObject
{
public TargetObject(string s)
{
Console.WriteLine("constructor invoked !!! " + s);
}
public void foo()
{
Console.WriteLine("foo() invoked !!!");
}
}

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

TargetObject 클래스가 MarshalByRefObject가 아닌 Context BoundObject에서 파생됐음과 Main 메쏘드에서 new 연산자를 이용해 TargetObject를 생성했음을 유심히 살펴보자. <리스트 9>의 수행 결과는 다음과 같다.

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

ContextBoundObject
ContextBoundObject 는 잘 알려지지 않았지만 매우 중요한 클래스이다. ContextBoundObject는 MarshalByRefObject에서 파생된 클래스로서 ContextBoundObject 클래스에서 직/간접적으로 파생된 클래스의 인스턴스를 액세스할 때 반드시 프록시가 사용된다. 이것은 CLR과 클래스 라이브러리가 보장해 주는 것으로서 항상 ContextBoundObject에 대한 참조형(reference) 변수는 Transpa rent Proxy가 된다. <리스트 7>의 CreateInstance() 메쏘드 구현에서 base.CreateInstance 메쏘드를 호출했을 때 왜 실제 객체의 참조가 아닌 TP가 반환되었는가에 대한 답이 될 것이다.

ContextBoundObject의 의미론적인 설명은 이렇다. Context BoundObject에서 파생된 클래스의 인스턴스는 특정 문맥(context)에 종속적이며, CLR은 이 인스턴스가 항상 특정 문맥 하에서 수행되도록 보장한다. 이 때 문맥은 COM+의 문맥도, ASP.NET에서 등장하는 Context 객체도 아닌 프로그래머가 정의하는 문맥이다. 문맥의 정의는 컴포넌트(클래스) 설계자/개발자에 의해 정의되며, 코드로서는 ContextAttribute로서 구체화된다. 지면 관계상 문맥에 관계된 사항을 자세히 설명할 수 없으므로 예를 들어 간단히 설명하도록 한다.

어떤 클래스는 시스템 자원을 사용하기 때문에 특정 계정 하에서 수행돼야 한다고 가정해 보자. 만약 날 코딩으로 작성한다면 클래스의 모든 메쏘드의 시작 부분에서는 계정을 바꾸고 메쏘드 종료 부분에서 계정을 원상태로 복구하는 코드를 필요로 하게 될 것이다. 하지만 사용자 계정 상태를 Context로 정의하고 ContextBoundObject를 사용하면 수작업 양을 크게 줄일 수 있을 뿐더러 재사용이 가능한 문맥을 정의할 수 있게 된다. 이것이 AOP가 아니면 무엇을 AOP라고 말할 수 있단 말인가?

다음은 앞서 설명한 예제를 간단한 의사 C# 코드로 표현한 것이다. 간단한 것 같지만 추가적으로 메시지 싱크(message sink)에 대한 이해를 필요로 하며 이들 메시지 싱크들의 체인이 어떻게 구성되는가 역시 이해를 해야만 한다. 물론 적절한 메시지 싱크의 구현도 뒤따라야 할 것이다. 이번 컬럼에서는 ContextBoundObject와 메시지 싱크에 대해 더 이상 다루지 않을 것이다. 이들에 대한 상세한 내용은 다음 컬럼에서 깊게 다룰 것을 약속한다.

class SetUserAttribute : ContextAttribute {
// 생략}

[SetUser(UserID=”administrator”)]
class SomeWorker : ContextBoundObject {
// 생략
}

1309 view

4.0 stars