본문 바로가기

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

개발 프레임워크 만들기 대장정 27 - Spring.NET의 advice 종류와 적용

■ advice 종류

▶ around advice

앞에서 알아본 CommonLoggingAroundAdvice 타입은 around advce중의 하나였다. 즉 인터페이스 IMethodInterceptor를 상속해서 Invoke() 메소드를 구현하고 있다. 아래는 IMethodInterceptor의 정의이다.

namespace AopAlliance.Intercept

{

    public interface IMethodInterceptor : ...

    {

           object Invoke(IMethodInvocation invocation);

    }

}

이 메소드의 인자로 넘어오는 invocation은 인터셉트된 타겟 객체에 대한 호출을 나타낸다. 이 인자의 Proceed() 메소드를 호출하면 인터셉트되어서 중단된 타겟 메소드 호출이 계속 진행된다.. Invoke()의 간단한 구현 코드이다.

...

public object Invoke(IMethodInvocation invocation)

{

    Console.WriteLine("Before invocation");

    object returnValue = invocation.Proceed();

    Console.WriteLine("After invocation and before return");

    return returnValue;

}

...

Proceed()가 호출되기 전에 원하는 작업을 할 수 있고, 호출된 후 그리고 반환값이 클라이언트 코드로 넘어가기 전에 또한 원하는 작업을 할 수 있다. 이곳에서 리턴되는 반환값을 조작해서 추가 정보를 넣거나 또는 제거할 수도 있다. 이처럼 타겟 메소드 호출 전, 후에 원하는 작업을 할 수 있는 advice를 around advice라고 한다.

참고로 이 advice가 설정되는 xml을 다시 보면 아래와 같다.

\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>


<!--타겟객체-->


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

<!-- Applies AOP on the contact service. -->

<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>

이렇게 설정해 놓으면 타겟 객체( calculator)의 메소드가 호출이 될때 commonLoggingAroundAdvice의 Invoke()에 의해서 인터셉트된다는 것이다. 가릿?

▶ before advice

before, after advice는 이름에서 예상되는 것처럼 호출전에 또는 호출된 후( 반환값을 반환하기전에)에만 끼어들기가 가능한 좀 더 간단한 advice이다.  before advice에서는 타겟 객체를 호출하기 위해서 Proceed()를 호출할 필요가 없다.  before advice를 구현하려면 인터페이스 IMethodBeforeAdvice를 상속해서 메소드 Before()를 구현해야 한다. 

public interface IMethodBeforeAdvice : ...

{

    void Before(MethodInfo method, object[] args, object target);

}

Before() 메소드는 타겟 메소드가 호출되기 전에 Spring.NET 프레워크에 의해서 호출된다. 이곳에서 필요한 사용자 정의 작업을 할 수 있다. method는 현재 인터셉트된 메소드에 대한 정보이고 args는 그 메소드를 호출할때 넘겨준 인자들에 대한 정보를 가지고 있다. 그리고 target은 타겟 객체에 대한 참조를 가지고 있다.

CommonLoggingAroundAdvice와 유사하게 CommonLoggingBeforeAdvice같은 advice를 구현해 놓고 앞에서처럼 설정을 하면 타겟 객체의 메소드들이 호출되기 전에 Before()가 호출된다는 것이다. 가릿? 가릿!

Before()를 수행하다 예외가 발생하면 이 예외는 이 메소드를 호출한 호출자로 전달된다.

▶ after advice

after advice 객체를 구현해서 설정해놓으면 타겟 메소드가 호출된 후에 클라이언트 코드로 반환되기 전에 호출될 수 있는데, 호출되는 메소드는 IAfterReturningAdvice 인터페이스를 구현한 객체의 AfterReturning()이다.

public interface IAfterReturningAdvice : ...

{

    void AfterReturning(object returnValue, MethodInfo method, object[] args, object target);

}

AfterReturning() 메소드에 전달되는 인자를 통해서 반환값, 타겟 메소드등에 대한 정보에 접근할 수 있다.

before advice에서는 예외가 발생하면 실행 경로를 역으로 진행해서 호출한 호출자에게 예외를 전달했다. 그러나 after advice는 실행 경로를 계속 유지한다. 그러나 리턴값을 반환하지 않고 예외를 반환한다.  실행 경로(excution path)란 advice 체인이 실행되는 순서를 말하는데 아래의 advice 체인에서 보여주는 그림을 참조하라.

▶ throws advice

throws advice 객체는 예상대로 타겟 객체에서 예외가 발생할때 호출되는 객체이다. 정확히는 throws advice가 적용된 후의 실행 경로상에서 예외가 발생할때 호출된다. 자세한 내용은 뒤의 "advice 체인"을 설명하는 곳을 참조한다.  이 녀석도 다른 녀석들처럼 throws advice가 되기 위해서는 상속해야 하는 인터페이스가 있다.

public interface IThrowsAdvice : IAdvice

{

}

근데 이 녀석은 다른 녀석들과는 다르게 구현해야 하는 메소드는 없다. 단지 구현되는 타입이 throws advice임을 나타내기만 하는 마커 인터페이스(marker interface 또는 태그 인터페이스 tag interface라고도 한다)이다. 그럼 예외가 발생했을때 Spring 프레임워크는 구현체의 어떤 메소드를 호출하게 될까. 구현체의 AfterThrowing() 메소드를 호출한다. Spring 프레임워크에 하드 코딩되어 있다고 볼 수 있겠다. AfterThrowing() 메소드에 전달되는 인자도 다음 두 유형중의 하나여야 한다.

void AfterThrowing(Exception ex)

void AfterThrowing( MethodInfo method, Object[] args, Object target, Exception ex)

다음은 실제 구현체에 대한 간단한 예제이다.

public class ConsoleLoggingThrowsAdvice : IThrowsAdvice

{

    public void AfterThrowing(Exception ex) // 실제로 이렇게 두 메소드이 예외 타입이 동일하게 구현하면 에러난다. 이유는 조금 아래에 있다.

    {

        // 예외정보로 필요한 예외 처리를 한다.

    }


    public void AfterThrowing(MethodInfo method, Object[] args, Object target, Exception ex)

    {

        // 메소드, 호출 인자, 타겟 객체에 대한 정보, 예외 정보로 필요한 예외 처리를 한다.

    }

}

그럼 왜 다른 advice처럼 아래와 유사한 형식으로 인터페이스를 정의하지 않았을까.

public interface IThrowsAdvice : IAdvice

{

    void AfterThrowing(Exception ex); // 왜 이와 유사한 메소드를 정의하지 않았을까?

    void AfterThrowing(MethodInfo method, Object[] args, Object target, Exception ex)

}

이렇게 하지 못하는(아니 하지 않는) 이유는 Spring 프레임워크에서 AfterThrowing() 메소드로 전달되는 예외 타입별로 핸들링을 할 수 있는 구조를 제공하기 위한 것이다. 다음과 같은 예외 처리 구조에 대해서는 익히 알고 있을 것이다.

//사용자 정의 예외 객체

public class MyException : Exception

{

}

...

public void method()

{

    try

    {

     // 작업...

    }

    catch (SqlException ex1)

    {

        // SqlException  예외를 처리한다.

    }

    catch (MyException ex2)

    {

        // MyException 예외를 처리한다.

    }

    catch (Exception ex3)

    {

        //Exception 예외를 처리한다.

    }

}

예외별로  다른 처리를 하고 싶다면 이런 구조적인 예외 메커니즘을 이용한다.

Spring.NET에서도 이런 유사한 구조를 제공하고자 한다. 해서 예외가 발생하면 Spring 프레임워크는 그 예외의 타입을 인식해서 적용된 throws advice에 구현되어 있는 AfterThrowing() 메소드들의 마지막 인자 즉 예외 객체의 타입과 비교를 한다. 그래서 만약 일치하는 예외 타입의 인자를 갖거나 또는 일치하는 예외 타입이 없다면 호환될 수 있는 예외 타입을 가지고 있는 AfterThrowing()을 호출한다. 만약 정의된 메소드중에서 발생한 예외의 타입과 호환되는 예외 타입의 인자를 갖는 예외 핸들링 메소드가 없다면 발생한 예외는 상위의 호출자로 버블링된다.

만약 AfterThrows() 메소드들중에서 발생한 예외 객체의 타입과 동일한 타입의 예외 인자를 갖는 메소드가 두개이상이라면? 런타임시 예외가 발생한다.

첫줄을 보면 하나의 메소드( AfterThrowing())안에 동일한 예외 타입의 인자를 갖는 메소드가 동시에 정의될 수 없다는 내용이다. 앞에서 예로 든 ConsoleLoggingThrowsAdvice 코드는 따라서 잘못된 것이다. 앞의 코드는 다음처럼 수정해서, AfterThrowing()의 마지막 인자인 예외 타입은 서로 달라야 한다.

public class ConsoleLoggingThrowsAdvice : IThrowsAdvice

{

    public void AfterThrowing(Exception ex)

    {

        Console.Out.WriteLine("Exception handler applied");

    }

    public void AfterThrowing(MyException ex)

    {

        Console.Out.WriteLine("MyException handler applied");

    }

    public void AfterThrowing(MethodInfo method, Object[] args, Object target, SqlException ex)

    {

        Console.Out.WriteLine("SqlException handler applied");

    }

}

얘기가 길어졌다. 이제 앞에서 던진 질문, throws advice에서 IThrowsAdvice에 AfterThrowing() 메소드를 포함하고 있지 않은 이유를 생각해보자. 간단하다. AfterThrowing() 메소드의 마지막 인자 즉 예외 객체의 타입을 미리 알 수 없다는 것이다. 사용자 정의 예외 타입을 사용한다면 어떻게 미리 알 수 있어서 인터페이스 메소드로 포함시키겠는가.

public interface IThrowsAdvice : IAdvice

{

    void AfterThrowing(MyException ex);//??

}

IThrowsAdvice를 상속하는 모든 구현체는 이 예외 타입의 메소드를 구현해야 한다는 얘기다. 해서 Spring에서는 이 방법을 버리고 런타임시, 실제 발생한 예외의 타입과 구현되어 있는 예외 타입을 비교해서 어떤 AfterThrowing()을 호출할지를 결정하는 방법을 선택했을 것이라는 순전히 개인적인 추측이다. 다른 이유가 있는지는 모르겠다.


■  advice 체인과 advice 실행 순서


앞에서 계속 미뤘던 advice 체인 개념을 알아보자. 타겟 객체의 하나의 pointcut에 대해서 하나만 advice를 적용할 수 있는 것은 아니다. 하나의 pointcut에 대해 앞에서 설명한 여러 종류의 advice 객체들을 여러개 적용할 수 있다. 타겟 객체 앞에 여러개의 advice가 체인처럼 연결되어 놓여져 있다. 타겟 메소드를 호출하면 체인처럼 설정되어 있는 모든 advice들을 호출에 적용하고 나서 최종적으로 타겟 메소드가 호출되는 것이다.  다음은 여러개의 advice들을 적용한 설정 예제이다.

<objects xmlns="http://www.springframework.net">


  <object id="beforeAdvice1"

          type="Spring.AopQuickStart.Aspects.ConsoleLoggingBeforeAdvice1, Spring.AopQuickStart.Common" />


  <object id="beforeAdvice2"

          type="Spring.AopQuickStart.Aspects.ConsoleLoggingBeforeAdvice2, Spring.AopQuickStart.Common" />


  <object id="afterAdvice1"

    type="Spring.AopQuickStart.Aspects.ConsoleLoggingAfterAdvice, Spring.AopQuickStart.Common" />


  <object id="aroundAdvice1"

          type="Spring.AopQuickStart.Aspects.ConsoleLoggingAroundAdvice, Spring.AopQuickStart.Common" />


  <object id="throwsAdvice1"

          type="Spring.AopQuickStart.Aspects.ConsoleLoggingThrowsAdvice, Spring.AopQuickStart.Common" />


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

    <property name="Target">

      <object type="Spring.AopQuickStart.Commands.ServiceCommand, Spring.AopQuickStart.Common" />

    </property>

    <property name="InterceptorNames">

      <list>

        <value>throwsAdvice1</value>       

        <value>beforeAdvice1</value>

        <value>aroundAdvice1</value>

        <value>afterAdvice1</value>

        <value>beforeAdvice2</value>

      </list>

    </property>

  </object>


</objects>

before, around, after, throws advice들이 모두 적용되었고 그리고 before advice는 ConsoleLoggingBeforeAdvice1, ConsoleLoggingBeforeAdvice2 두 개가 적용되었다. 다음과 같은 advice 체인을 상상할 수 있다.

여기서 생각해 볼 문제가 하나 있다. advice가 실행되는 순서이다. 예제처럼 beforeAdvice2가 afterAdvice1보다 뒤에 설정되어 있다고 해서 그 적용 순서도 뒤일까. 이렇게 되면 말이 되지 않는다. after advice는 타겟 객체를 호출하고 나서 적용되는  advice이고 before advice는 타겟 객체가 호출되기 전의 advice이다. 설정은 뒤에 오더라도 적용되는 순서는 beforeAdvice2가 먼저여야 한다.

정리하면 advice 체인의 순서가 실제 적용되는 순서는 아니라는 것이다. advice 타입에 따라서 그 적용 순서는 바뀔 수 있다. 다음 그림은 예제에서 설정된 advice들이 실행되는 순서를 그림으로 보여주고 있다.

다음은 앞에서의 예제대로 설정해서 샘플 프로그램을 작성해서 실행한 결과이다.

여기서 한가지 주의할 것은 throws advice는 제일 먼저 설정되어야 한다는 것이다. 그래야 이 후의 advice 적용시 예외가 발생하더라도 그 예외도 설정한 throws advice에서 핸들링할 수 있다. throws advice가 적용되기 전에 중간의 advice에서 예외가 발생하면 그 예외에 대해서는 throws advice가 적용되지 않는다.


지금까지 AOP에 대한 이야기였다. Spring.NET의 중요한 구성 요소중의 하나가 AOP 프레임워크이지만, Spring.NET의 IoC 컨테이너가 AOP에 종속되는 것은 아니다. 이것은 원하지 않는다면 AOP를 사용하지 않고도 Spring.NET 프레임워크를 사용할 수 있다는 것이다.

실전 프로젝트에서도 Spring.NET 원형 그대로를 사용할 수도 있겠지만, Spring.NET 프레임워크를 기반으로 해서 그 프로젝트 상황에 맞도록 한단계 더 추상화된 개발 프레임워크를 제작할 수도 있을 것이다. 이때 다시 프레임워크를 만드는 입장이 되어서 AOP를 사용하면 프레임워크다운 프레임워크가 될 수 있을 것이다. 개발자에게 다양한 Cross cutting concerns에 걸쳐서 동일한 코드를 Copy&Paste하도록 하는 것보다는 하나의 advice 모듈로 구현한 것을 공통팀에서 관리하는 것이 훨씬 더 효율적일 것이라는 것이다. 남은 것은 이제 advice로 구현할 수 있는 cross cutting concerns를 추상화하고 설계하는 것이다.

다음 포스트에서는 별다른 주제가 없으면 Spring.NET이 지원하는 ASP.NET Web Services에 대한 이야기가 될 것이다.