본문 바로가기

IT 살이/04. 기술 - 프로그래밍

개발 프레임워크 만들기 대장정 25 - AOP 적용 예제 I

■ 예제 설명

앞에서 본 샘플 프로젝트 솔루션의 구조이다.

Spring.Calculator.Web 프로젝트를 실행시켜보면 다음과 같은 결과 페이지가 보인다.

첫번째 링크는 단순한 웹 서비스 메소드를 호출하고 있다. AOP가 적용된 메소드를 호출하기 위해서는 두번째 링크를 클릭해야 한다. 이번 포스트에서는 두번째 링크에 대한 웹 서비스를 AOP 예제로 삼겠다.  두번째 링크를 클릭하면 다음과 같은 웹 서비스 테스트 화면이 나온다.

노출된 메소드중에서 Add 메소드를 클릭해서 적절히 값을 넣고 호출한다.

이 메소드를 호출하고 나서 남는 로그는 다음과 같다. 

2008-08-18 23:09:34,406 [DEBUG] Spring.Aspects.Logging.CommonLoggingAroundAdvice - Intercepted call : about to invoke method 'Add'

2008-08-18 23:09:34,421 [DEBUG] Spring.Aspects.Logging.CommonLoggingAroundAdvice - Intercepted call : returned '2'

그러나 웹 서비스 메소드에는 로그를 남기는 코드는 없다. 별도의 advice를 사용해서 로그를 남기고 있다. 이 샘플에서는 AOP를 구현하기 위해서 개발자가 해야 할 일을 앞에서 설명한 대로 차례로 진행해보자. 


■ 타겟 객체 정의


먼저 타겟 객체에 대한 소스를 보자. 먼저 두번째 링크가 호출하는 클래스는 Spring.Calculator.Services.2005 프로젝트의 AdvancedCalculator 를 보면 다음과 같다.

public class AdvancedCalculator : Calculator, IAdvancedCalculator

{

    #region Fields

    private int memoryStore = 0;

    #endregion


    #region Constructor(s) / Destructor

    public AdvancedCalculator()

    {}


    public AdvancedCalculator(int initialMemory)

    {

        memoryStore = initialMemory;

    }

    #endregion


    #region IAdvancedCalculator Members

    public int GetMemory()

    {

        return memoryStore;

    }

    public void SetMemory(int memoryValue)

    {

        memoryStore = memoryValue;   

    }

    public void MemoryClear()

    {

        memoryStore = 0;

    }

    public void MemoryAdd(int num)

    {

        memoryStore += num;

    }


    #endregion

}

이 클래스에는 Add 메소드가 없다. 상속을 하고 있는 부모 클래스 Calculator에서 구현하고 있다. 그리고 앞 포스트에서 말한대로 타겟 객체가 되기 위해서는 현재 버전의 Spring.NET( v 1.1.2)에서는 반드시 인터페이스를 구현해야 한다고 했다. 코드를 보면 IAdvcancedCalculator를 상속해서 구현하고 있다.

public interface IAdvancedCalculator : ICalculator

{

    int GetMemory();

    void SetMemory(int memoryValue);

    void MemoryClear();

    void MemoryAdd(int num);

}

IAdvancedCalculator 인터페이스는 ICalculator를 상속해서 인터페이스 정의를 물려받고 있다. ICalculator 인터페이스와 그것을 구현하고 있는 Calculator 클래스 코드는 다음과 같다.

public interface ICalculator

{

    int Add(int n1, int n2);

    int Substract(int n1, int n2);

    DivisionResult Divide(int n1, int n2);

    int Multiply(int n1, int n2);

}

public class Calculator : ICalculator

{

    #region ICalculator Members


    public int Add(int n1, int n2)

    {

        return n1 + n2;

    }


    public int Substract(int n1, int n2)

    {

        return n1 - n2;

    }


    public DivisionResult Divide(int n1, int n2)

    {

        DivisionResult result = new DivisionResult();

        result.Quotient = n1 / n2;

        result.Rest = n1 % n2;

        return result;

    }


    public int Multiply(int n1, int n2)

    {

        return n1 * n2;

    }


    #endregion

}

인터페이스들은 구현 클래스들과는 다른 프로젝트 Spring.Calculator.Contract.2005에 구현되어 있다. 만약 클라이언트 애플리케이션과 서버 애플리케이션이 분리되어 있다면 클라이언트에서는 인터페이스 어셈블리만 참조하면 된다. 물론 서버측에서는 인터페이스 어셈블리와 구현 어셈블리가 같이 참조되어야 한다.


■ advice 코딩하기


이제 타겟 객체를 호출할때 weaving될 advice 코드를 살펴본다. 샘플에서는 프로젝트 Spring.Aspects.2005에 구현되어 있다.

현재는 두개의 로깅 advice가 구현되어 있다. 이 중에서 웹 애플리케이션에서는 CommonLoggingAroundAdvice를 사용해서 로그를 남기고 있다. 이 advice 코드를 보면 다음과 같다.

public class CommonLoggingAroundAdvice : IMethodInterceptor

{

    #region Logging

    private static readonly ILog LOG = LogManager.GetLogger(typeof(CommonLoggingAroundAdvice));

    #endregion


    #region Fields

    private LogLevel _level = LogLevel.All;

    #endregion


    #region Properties

    public LogLevel Level

    {

        get { return _level; }   

        set { _level = value; }

    }

    #endregion


    #region IMethodInterceptor Members


    public object Invoke(IMethodInvocation invocation)

    {

        Log("Intercepted call : about to invoke method '{0}'", invocation.Method.Name);

        object returnValue = invocation.Proceed();

        Log("Intercepted call : returned '{0}'", returnValue);

        return returnValue;

    }


    #endregion


    #region Private Methods

    private void Log(string text, params object[] args)

    {

        switch(Level)

        {

            case LogLevel.All :

            case LogLevel.Debug :

                if (LOG.IsDebugEnabled) LOG.Debug(String.Format(text, args));

                break;

            case LogLevel.Error :

                if (LOG.IsErrorEnabled) LOG.Error(String.Format(text, args));

                break;

            case LogLevel.Fatal :

                if (LOG.IsFatalEnabled) LOG.Fatal(String.Format(text, args));

                break;

            case LogLevel.Info :

                if (LOG.IsInfoEnabled) LOG.Info(String.Format(text, args));

                break;

            case LogLevel.Warn :

                if (LOG.IsWarnEnabled) LOG.Warn(String.Format(text, args));

                break;

            case LogLevel.Off:

            default :

                break;

        }

    }

    #endregion

}

이 advice는 타겟 객체의 메소드를 호출할때 적용된다. 타겟 객체의 어디서, 어떻게 적용될지를 선택할 수 있는 방법이 바로 IMethodInterceptor 인터페이스이다. 이 인터페이스에서는 단지 object Invoke()만을 정의하고 있다.

IMethodInterceptor를 구현하고 있는 advice가 타겟 객체에 적용될때는 타겟 객체의 메소드를 호출하면 항상 IMethodInterceptor인터페이스의 Invoke()가 호출된다. 코드에서 Invocation.Proceed(); 부분이 advice가 캡쳐한 원래의 호출을 다시 타겟 객체로 전달하는 부분이다. 타겟 객체로 호출을 전달하기 전에 예제의 advice에서는 로그를 남기는 작업을 하고 있다. 로그를 남기는 Log()에 대해서는 지금 이곳에서는 중요한 부분이 아니므로 넘어가도록 한다. Proceed()를 호출하고 나서 반환값을 받고서도 클라이언트로 바로 넘기지 않는다. 반환되기 전의 순간도 캡쳐할 수 있다. 예제 advice에서는 타겟 객체에서 반환하는 값에 접근해서 그 값을 로그로 남기고 있다. 그런 다음 최종적으로 클라이언트 코드로 반환값을 넘겨주고 있다. 만약 타겟 객체가 개발자가 개발g한 비즈니스 객체이고 개발 프레임워크에서 이와 같은 advice를 개발해서 적용한다면 얼마나 유용할지 짐작이 갈 것이다.

이렇게 타겟 메소드의 호출 전 후를 캡쳐할 수 있는 기회를 제공하는 advice를 "around advice"라고 한다. IMethodInterceptor는 around advcie의 Spring 프레임워크의 구현이다. 그외에도 before advice, after advice, throws advice등이 있고 이것을 각각 구현한 Spring.NET의 인터페이스들이 있다. 이것에 대해서는 뒤에서 다루기로 하고 지금은 Spring.NET 애플리케이션에서 AOP를 적용하는 전체적인 절차를 계속 알아보도록 하자.

지금까지의 내용을 보면, 인터페이스가 있었고 그리고 인터페이스를 구현한 타겟 객체가 있었다. 그리고 타겟 객체의 메소드를 호출할때 적용될 advice가 있었다. 이제 실제로 advice를 타겟 객체에 적용하기 위한 설정이 필요하다.


■  advice 적용 설정하기( ProxyFactoryObject 설정하기 )


advice를 타겟 객체에 적용하기 위한 설정은 다시 말하면 ProxyFactoryObject 객체 설정과 같은 말이다. 앞의 포스트에서 Spring.NET에서는 프락시 패턴을 이용해서 AOP를 구현하고 있다고 했다. 그리고 advice가 적용된(weaving된) 프락시를 AOP 프락시로 표현했는데, 이 AOP 프락시를 런타임시에 동적으로 생성해내는 객체가 바로 ProxyFactoryObject라고 했다. 클라이언트 코드에서는 타겟 객체에 대한 참조와 advice를 건네주고 ProxyFactoryObject객체로부터 타겟 객체에 대한 AOP 프락시를 받는다고 했다. 이 시나리오를 설정을 통해서 구현하면 다음과 같다. 이 시나리오를 코드상에서 프로그램적으로 구현할 수 있는 API도 제공하고 있다. 이 설정은 웹 프로젝트 Spring.Calculator.Web.2005 의 web.config의 부분이다.

\par ??\par ??<\cf13 object\cf2 \cf6 id\cf2 =\cf0 "\cf2 CommonLoggingAroundAdvice\cf0 "\cf2 \cf6 type\cf2 =\cf0 "\cf2 Spring.Aspects.Logging.CommonLoggingAroundAdvice, Spring.Aspects\cf0 "\cf2 >\par ??\tab <\cf13 property\cf2 \cf6 name\cf2 =\cf0 "\cf2 Level\cf0 "\cf2 \cf6 value\cf2 =\cf0 "\cf2 Debug\cf0 "\cf2 />\par ??\par ??\par ??\par ??\par ??<\cf13 object\cf2 \cf6 id\cf2 =\cf0 "\cf2 calculator\cf0 "\cf2 \cf6 type\cf2 =\cf0 "\cf2 Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services\cf0 "\cf2 />\par ??<\cf13 object\cf2 \cf6 id\cf2 =\cf0 "\cf2 calculatorWeaved\cf0 "\cf2 \cf6 type\cf2 =\cf0 "\cf2 Spring.Aop.Framework.ProxyFactoryObject, Spring.Aop\cf0 "\cf2 >\par ??\tab <\cf13 property\cf2 \cf6 name\cf2 =\cf0 "\cf2 target\cf0 "\cf2 \cf6 ref\cf2 =\cf0 "\cf2 calculator\cf0 "\cf2 />\par ??\tab <\cf13 property\cf2 \cf6 name\cf2 =\cf0 "\cf2 interceptorNames\cf0 "\cf2 >\par ??\tab \tab <\cf13 list\cf2 >\par ??\tab \tab \tab <\cf13 value\cf2 >\cf0 CommonLoggingAroundAdvice\cf2 \par ??\tab \tab \par ??\tab \par ??\par ??} -->

<!-- Aspect -->


<object id="CommonLoggingAroundAdvice" type="Spring.Aspects.Logging.CommonLoggingAroundAdvice, Spring.Aspects">

  <property name="Level" value="Debug"/>

</object>


<!-- Service -->


<object id="calculator" type="Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services"/>

<object id="calculatorWeaved" type="Spring.Aop.Framework.ProxyFactoryObject, Spring.Aop">

  <property name="target" ref="calculator"/>

  <property name="interceptorNames">

    <list>

      <value>CommonLoggingAroundAdvice</value>

    </list>

  </property>

</object>

첫번째 <object/>요소에 타겟 객체에 적용될 advice가 정의되어 있다. id는 CommonLoggingAroundAdvice로 하고 있는데, 다른 <object/>에서 이 객체를 참조할때 id 값을 이용할 수 있다. advice는 Spring.Aspects.Logging 네임스페이스에 포함된 CommonLoggingAroundAdvice 클래스에 정의되어 있다는 것을 type 어트리뷰트값을 통해서 나타내고 있다.

두번째 <object/> 요소에서는 타겟 객체에 대한 정의를 표현하고 있다. id는 calculator로 하고 있고 타겟 객체의 타입은 어셈블리 Spring.Calculator.Services의 Spring.Calculator.Services 네임스페이스 아래에 있는 AdvancedCalculator 클래스에서 정의하고 있다.

세번째 <object/>요소는 바로 앞의 advice객체와 타겟 객체를 인자로 받아들이는 AOP 인터페이스 제너레이터 ProxyFactoryObject에 대한 정의이다. ProxyFactoryObject 타입의 속성중에는 Target, InterceptorNames( 대소문자 무관)가 있는데 Target 속성을 통해서 타겟 객체에 대한 참조를 받고, InterceptorNames 속성을 통해서 around advice를 받고 있다. 타겟 객체에 대한 참조를 지정할때 <property/>요소의 ref 어트리뷰트를 사용하는데 그 값으로는 앞에서 AdvancedCalculator 객체를 정의하고 있는 <object/>의 id 어트리뷰트를 지정하고 있다. 그리고 InterceptorNames 속성은 여러개의 advice를 지정할 수 있다. 그래서 <list/>요소 내부에 <value/> 요소를 사용해서 advice를 추가하고 있는데, 다른 advice 객체가 있다면 <value/>를 더 추가할 수 있다. 여기서 <value/>의 값으로 지정된 CommonLoggingAroundAdvice는 advice를 정의하고 있는 첫번째 <object/>요소의 id값이다.

이로써 특정 advice를 특정 타겟 객체에 적용하는 작업은 끝났다.  클라이언트 코드에서는 이제 다음과 같은 방법으로 AOP 프락시에 대한 참조를 얻을 수 있다.

IApplicationContext ctx = ContextRegistry.GetContext();

IAdvancedCalculator firstCalc = (IAdvancedCalculator) ctx.GetObject("calculatorWeaved");

컨텍스트 객체의 GetObject() 메소드에 ProxyFactoryObject 객체에 대한 id값을 넘겨주면 원한는 타겟 객체에 대한 AOP 프락시를 받을 수 있다. 반환되는 객체가 ProxyFactoryObject 객체 자체가 아니라 그 타겟 객체에 대한 프락시임을 다시 한번 더 상기하자. 이제 AOP 프락시를 통해서 클라이언트측 코딩을 해 나가면 된다.

현재 웹 샘플 Spring.Calculator.Web.2005 에서는 클라이언트 코드에서 타겟 객체에 대한 참조를 이용하는 코드가 없다. 현재의 웹 샘플 코드에서는 서버측에서만 AOP를 적용하는 코드가 있다. 타겟 객체의 메소드를 호출하는 클라이언트 코드는 앞의 그림과 같은 ASP.NET에서 제공하는 테스트 페이지를 사용하고 있다. 앞에서와 유사한 코드는 프로젝트 Spring.Calculator.ClientApp.2005의 Program.cs 파일에 있다.

Spring.Calculator.Web.2005 프로젝트에 있는 web.config의 설정은 웹 서비스로 노출된 타겟 객체에 대한 설정이다. 따라서 클라이언트측 로그는 없지만 서버측 객체 호출에 대한 로그는 남는다.


■  웹 애플리케이션 실행하기


Logs폴더 하위의 log.txt 파일을 열어보면 웹 서비스 메소드가 호출될때 남겨진 로그를 볼 수 있다. 참고로 실행중에는 VS.NET의 솔루션에서 오픈하지 말고, 윈도우 탐색기에서 오픈하라. 다음은 메소드 Add()와 Divide()를 호출한 후에 남은 로그 내용이다.

2008-08-20 00:08:52,468 [DEBUG] Spring.Aspects.Logging.CommonLoggingAroundAdvice - Intercepted call : about to invoke method 'Add'
2008-08-20 00:08:52,484 [DEBUG] Spring.Aspects.Logging.CommonLoggingAroundAdvice - Intercepted call : returned '3'
2008-08-20 00:09:06,078 [DEBUG] Spring.Aspects.Logging.CommonLoggingAroundAdvice - Intercepted call : about to invoke method 'Divide'
2008-08-20 00:09:06,078 [DEBUG] Spring.Aspects.Logging.CommonLoggingAroundAdvice - Intercepted call : returned 'Quotient: '2'; Rest: '1''


지금까지의 설정처럼 하면 타겟 객체의 모든 메소드를 호출할때마다 CommonLoggingAroundAdvice의 내용이 적용될 것이다. 그러나 때로는 타겟 객체의 특정 메소드를 호출하는 경우에만 설정한 advice들이 적용되기를 바랄 수도 있을 것이다. 


■ pointcut 코딩하기


타겟 객체의 특정 메소드만 호출할때 로그를 남기고 싶다면 앞에서와 같은 기본적인 설정만으로는 부족하다. 해서 좀 더 특별한 설정이 필요하다. 특별한 설정이란 바로 타겟 객체의 pointcut을 지정하는 것이다. AOP의 일반적인 이론에서는 여러 joinpoint( advice가 weaving될 수 있는 포인트. 예를 들어 속성 값이 변하기 전, 후)가 있을 수 있겠지만, 현재 AOP 프락시를 이용하여 AOP를 구현하는 방법에서는 메소드만이 pointcut의 대상이 된다. 말이 점점 어려워진다 -_-;;

여튼 현재의 Spring.NET 버전에서는 메소드만이 advice가 적용될 수 있고 메소드중에서 특별한 메소드만 advice가 적용될 수 있도록 하는 방법이 있다. Spring.Aop.Support네임스페이스 아래에 있는 RegularExpressionMethodPointcutAdvisor 타입을 이용해서 그런 설정을 할 수 있는데, 이 타입은 정규식을 이용하고 있다. 즉 특정 정규식에 일치하는 메소드명을 갖는 타겟 메소드에만 advice를 적용시키는 설정을 할 수 있다.

불행히도 현재 사용하고 있는 샘플 프로젝트중에는 이 설정이 없다.  설정을 조금 수정해야 한다.  시간 관게상 이 작업은 다음 포스트에서 하도록 한다.