프록시를 이용한 AOP 예제, 로깅 지금까지 커스텀 프록시를 작성하기 위한 기본적인 사항들을 대부분 살펴봤다. 이제 프록시에 대한 실제적인 예제를 작성해 보자.
AOP
의 개념을 설명할 때 자주 등장하는 것이 로깅이다. 메쏘드를 호출할 때마다 호출에 대한 로그를 남기는 것이다. 프록시를 사용하지
않는다면 메쏘드 시작 부분과 종료 부분에서 Trace.WriteLine 메쏘드를 호출하는 코드를 작성해야 할 것이다. 그것도
로깅이 필요한 모든 메쏘드에서 반복적으로 말이다. 하지만 프록시를 응용하면 한번의 코딩과 특성들을 사용하여 재사용 가능한 로깅
모듈을 작성할 수 있다. 먼저 필요한 것은 ProxyAttribute를 정의하는 것이다. ProxyAttribute의 구현은
클래스 이름이 SimpleProxyLog만 다를 뿐 <리스트 7>과 동일하다. 따라서 여기서는 설명을 생략한다. 실제
구현은 ‘이달의 디스켓’을 참고하기 바란다.
로깅을 수행하는 프록시의 구현은 예전 프록시 예제보다 복잡해졌다. 실제
메쏘드 호출 전에 메쏘드 시작 로그를 남기고 메쏘드 종료 후에 다시 메쏘드 종료 로그를 남기기 위해 두 개의 새로운 메쏘드가
추가됐고, Invoke의 구현 역시 약간 복잡해졌다. Invoke 메쏘드에서는 실제 메시지 처리 전에 전처리 성격으로
MethodEnterLog 메쏘드를 호출하여 로그를 남긴다. MethodEnterLog 메쏘드는 호출된 메쏘드에
LogAttribute 특성이 존재할 때만 로그를 남긴다.
로그 형식은 메쏘드 이름, 매개변수 이름, 매개변수
값을 출력하는데, 이들 정보는 모두 IMethodCallMessage 인터페이스로부터 구할 수 있다. 특히 호출 대상인 메쏘드의
메타 정보를 제공하는 IMethodCall Message.MethodBase 프로퍼티는 매우 유용하다. 이 프로퍼티는
System.Reflection.MethodBase 타입의 메쏘드 메타 데이터를 제공하지만 대상 메쏘드가 생성자인지 아니면
단순한 인스턴스 메쏘드인지에 따라 MethodInfo 타입으로 형 변환도 가능하다. MethodInfo 타입은 메쏘드의 리턴
타입과 같이 자세한 정보를 제공한다.
IMethodCallMessage 인터페이스의 GetArgName 메쏘드와
GetArg 메쏘드의 사용법에도 한 번쯤 눈길을 줄 필요가 있다. 매개변수의 순서 값을 이용하여 매개변수 이름 및 매개변수
값까지 알아낼 수 있다. 하지만 IMethodCallMessage가 제공하는 정보만을 이용하여 매개변수가
call-by-value인지 아니면 call-by-reference인지를 알아낼 수 없다. 따라서 매개변수의 방향성, 즉 in,
ref, out을 판별하기 위해서는 메타 데이터에 의존해야 한다. MethodLeaveLog 메쏘드 역시 그다지 어렵지 않은
구현이다. IMethodReturn Message 인터페이스의 ReturnValue 프로퍼티를 이용하여 결과 값을 출력하는 것을 제외하고는 그다지 복잡한 구현이 아니므로 더 이상 설명이 필요 없으리라 본다.
<리스트 10> 로깅을 위한 프록시, SimpleLoggerProxy 구현 [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Property)] public class LogAttribute : Attribute { // 마크(mark)용 애트리뷰트이므로 내부 구현이 없다. }
public class SimpleLoggerProxy : RealProxy { private MarshalByRefObject m_Target;
public SimpleLoggerProxy(MarshalByRefObject target, Type serverType) : base(serverType) { m_Target = target; }
private bool MethodEnterLog(IMethodCallMessage callMsg) { // reflection을 통해 Log 특성이 메쏘드에 존재하는지 확인한다. MethodBase method = callMsg.MethodBase; object[] attributes = method.GetCustomAttributes(typeof(LogAttribute), false); if (attributes == null || attributes.Length == 0) return false; // Log 특성이 없다면 아무런 작업도 하지 않는다. StringBuilder sb = new StringBuilder(128); if (callMsg is IConstructionCallMessage) sb.Append(".ctor("); else sb.AppendFormat("{0} {1}(", ((MethodInfo)method).ReturnType.Name, method.Name); for(int i=0; i < callMsg.ArgCount; i++) { sb.AppendFormat("{0}={1}", callMsg.GetArgName(i), callMsg.GetArg(i)); if (i < callMsg.ArgCount - 1) sb.Append(','); } sb.Append(')'); Trace.WriteLine(sb.ToString(), "SM"); return true; }
private void MethodLeaveLog(IMethodReturnMessage retMsg) { if (retMsg is IConstructionReturnMessage) { Trace.WriteLine("end of construction", "EM"); } else { Trace.WriteLine("end of method : ret = " + retMsg.ReturnValue.ToString(), "EM"); } }
public override IMessage Invoke(IMessage msg) { IMethodCallMessage callMsg = msg as IMethodCallMessage; IMethodReturnMessage retMsg = null; bool fLog = false;
// 메쏘드 시작 로그 fLog = MethodEnterLog(callMsg);
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 { retMsg = RemotingServices.ExecuteMessage(m_Target, callMsg); } // 메쏘드 종료 로그 if (fLog) MethodLeaveLog(retMsg); return retMsg; }
}
<
리스트 10>의 SimpleLoggerProxy를 사용하는 예제를 살펴보자. 이 프록시를 사용하고자 하는 클래스는
ContextBoundObject에서 직/간접적으로 파생되고, 로그를 남기고자 하는 메쏘드에 LogAttribute 특성을
추가하면 된다. <리스트 11>에서 TestObject 클래스는 SimpleLoggerProxy 프록시를 사용한다.
주목할 점은 생성자나 foo() 메쏘드에 대한 호출은 로그를 남기는 반면에 bar() 메쏘드는 로그가 남지 않는다는 점이다.
이렇게 특성과 메쏘드 호출 가로채기 기법을 이용하여 컴포넌트가 다양한 영역에 사용되도록 해주는 것이 닷넷이 기본적으로 보유하고
있는 AOP에 대한 지원이다. J로 시작하는 모 프로그래밍 환경보다 훨씬 더 풍부하고 유연하고 능력을 갖는 개발환경이 아닌가?
<리스트 11> SimpleLoggerProxy를 사용하는 예제 [SimpleLoggerProxy] public class TestObject : ContextBoundObject { [Log] public TestObject() { Console.WriteLine("Some Construction Work..."); }
[Log] public void foo(int a, string b) { Console.WriteLine("foo()........"); }
public void bar() { Console.WriteLine("bar()........"); } }
public class TestCode { [STAThread] public static void Main() { TestObject obj = new TestObject(); obj.foo(5, "234"); obj.bar(); } }
다음 컬럼에서는 이
번 컬럼에서는 AOP의 개념을 아주, 매우, 정말로, 특별하게, 간략히 설명했고 닷넷에서 AOP에 대한 접근을 하기 위한
인프라가 무엇이 있는지 살펴봤다. 닷넷의 특성과 가로채기 인프라는 AOP를 위한 풍부한 프로그래밍 환경을 제공한다는 사실을
살펴봤다. 특히 MarshalByRefObject나 ContextBoundObject에서 파생된 클래스에 커스텀 프록시를
사용함으로써 객체의 수행 환경을 윤택하게 하거나 다양한 전처리/후처리가 가능하다는 사실 역시 알아봤다.
한 가지
아쉬운 것은 이것이 AOP와 관련된 끝이 아니라는 사실이다. ContextBoundObject 클래스와 그와 관련된 다양한
클래스, 특성, 인터페이스는 진정한 AOP를 향한 길을 제공한다. 특별히 커스텀 프록시를 작성할 필요 없이 메시지를 처리하는
컴포넌트(메시지 싱크)를 메시지 처리 체인에 삽입해 넣음으로써 이번 컬럼에서 설명한 모든 효과와 더불어 더욱 더 풍부한 컴포넌트
수행 환경을 구축할 수 있다. 그리고 이 프레임워크는 매우 표준화되어 있으며, 닷넷 리모팅과 같은 분야에 응용할 수도 있기
때문에 더욱 더 가치가 있다고 할 수 있다.
때문에 ContextBoundObject에 대해서는 더 많은 지면과
정력을 투입하여 살펴볼 가치가 있다고 판단되는 바 다음 컬럼에서는 ContextBoundObject에 대해 살펴보도록 하겠다.
당장 알고 싶은 성격이 급한 독자를 위해 참고자료에 관련 도서와 자료를 소개한다. 좀 느긋한 독자라면 다음 컬럼을 기대해 주기
바란다.
마지막으로 필자의 컬럼에 대한 의문이나 토론을 원하는 독자들은 주저 말고 메일이나 게시판을 이용해 주기
바란다. 잘못된 지적이나 기타 어떤 것도 환영한다. 필자는 독자와 이 컬럼을 통해, 그리고 다른 매체를 통해 대화하기를 간절히
바란다.