본문 바로가기

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

개발 프레임워크 만들기 대장정 24 - Aspect Oriented Programming 개념 II

바로 예제 설명으로 들어가려 했으나 아무래도 AOP 개념에 대해 좀 더 설명이 필요할 것 같다. Aspect Oriented Programming하면 떠올라야 하는 개념은 "타겟 객체에 대한 호출을 중간에서 인터셉트할 수 있는 방법"이라는 것이다. 개발 프레임워크 입장에서 생각해본다면 얼마나 근사한 구조인가. 타겟 객체( 개발자가 개발)에 대한 모든 호출( 개발자가 만든 코드에서의 호출)을 개발 프레임워크에서 캐취할 수 있다는 것은 많은 장점을 가지고 있다.

그리고 실제로 AOP를 구현하기 위해서 개발자가 AOP의 컨셉을 모두 개발할 필요는 없다. advice는 개발자가 C#문법을 이용해서 일반 객체를 정의하듯이 구현하면 된다. 그러나 advice나 pointcut 자체는 Spring.NET의 IoC 컨테이너가 관리한다. Spring.NET의 AOP를 구현하기위해서 개발자가 해야 할 일을 정리하면 다음과 같다.

▶ 타겟 객체 개발하기

타겟 객체란 AOP가 적용될 타입의 객체를 말한다. 예를 들어 다음에 설명하는 advice( 예로, 메소드 호출 전 후에 로그를 남기는 코드)를 적용하고 싶은 객체를 말한다. 애플리케이션용 클래스중에서 선택한 클래스가 AOP 프로그래밍의 타겟객체로 설정될 수 있다.  뒤에 설명하겠지만 타겟 객체가 되기 위해서는 인터페이스를 구현해야 하는 제한이 있다.

▶ advice 코딩하기

필요한 aspect를 코딩한다. 그러나 대부분의 구현은 이미 되어 있다:로깅, 트랜잭션, 보안 등.  예를 들어 로깅 모듈로 Log4net이 있다.  그리고 하나의 타겟 객체에 대한 여러개의 advice가 차례로 적용될 수 있다.

▶ pointcut 지정하기

pointcut을 지정할 수 있는 기본적인 방법 또한 Spring.NET에서 제공하고 있다. 예를 들어 정규식을 사용하는 방법으로 타겟 객체에서 "Do"로 시작하는 메소드에 대해서 Advice를 적용하라는 식의 설정을 할 수 있다.

▶ Advice를 타겟 객체에 적용하는 작업하기.

결정된 advice와 pointcut을 configuration하는 작업이다.


■ ProxyFactoryObject를 이용하여 AOP 프락시 생성하기


Spring.NET에서는 프락시 패턴을 이용해서 AOP를 적용할 수 있는 구조를 만든다.

그림을 보면 클라이언트 코드는 직접 타겟 객체를 참조하지 않고 프락시를 통해서 호출하고 있다. 프락시는 클라이언트의 호출을 받으면 바로 타겟 객체의 메소드를 호출하지 않는다. 3번 호출처럼 필요하다면  Aspect 모듈을 호출한다. 이때 Cross Concerns( advice로 구현된다)에서는 타겟 객체의 메소드를 호출하기 전에 advice의 내용(로깅, 트랜잭션 작업 시작 등)을 수행할 수 있다. 그리고 나서 타겟 객체의 메소드를 호출한다. 또한 타겟 객체로부터의 반환을 바로 클라이언트로 넘기지 않는다. 타겟 객체의 반환이 클라이언트로 넘겨주기 전에 또한 필요하다면 Aspect 모듈을 호출해서 마무리 작업( 호출후의 로깅작업, 트랜잭션 작업 등)을 할 수 있다. 그리고 나서 최종적으로 클라이언트에 반환값을 넘겨준다. 이처럼 AOP를 구현하기 위한 프락시를 "AOP 프락시"라고 한다. 이 AOP 프락시에는 타겟 객체의 메소드를 호출하는 코드만 있는 것이 아니라 구현된 advice를 호출하는 메소드 또한 포함되어 있다.

Spring.NET에서는 이런 AOP 프락시를 만드는 녀석이 바로 ProxyFactoryObject이다( 수정일자: 2009.05.24. AOP 프락시를 생성하는 방법은 여러가지가 있다. ObjectNameautoProxyCreator, DefaultAdvisorAutoProxyCreator 등. Spring.NET 레퍼런스 문서 13.5, 13.9절등을 참고한다.) 최종적으로 앞의 그림과 같은 실행 구조가 되기 위해서, 클라이언트는 프락시가 필요한 타겟 객체를 ProxyFactoryObject에게 넘겨주면 ProxyFactoryObject는 타겟 객체에 대해서 advice 호출을 포함하고 있는 AOP 프락시를 생성해서 클라이언트에 넘겨준다. AOP 프락시를 생성하는 작업은 런타임에 일어나는데, 프락시 클래스용 IL코드를 만들어 내기위해서 System.Reflection.Emit 네임스페이스의 클래스들을 사용한다. 

ProxyFactoryObject가 AOP프락시를 런타임시에 동적으로 생성한다는 것은 런타임시에 weaving(advice를 적절한 pointcut에 끼워넣는 작업)이 수행된다는 것이다.  weaving은 AOP를 구현하는 방식에 따라 컴파일시에 또는 클래스 로딩시에 수행될 수 있다. 그러나 Spring.NET에서는 현재 런타임시만을 지원하고 있다. 또한 현재 버전(version 1.1.2)에서는 AOP 프락시를 생성하고자 하는 타겟 객체는 반드시 하나 이상의 인터페이스를 구현해야 한다.  ( 반드시 하나 이상의 인터페이스를 구현해야 하는 제약은 버전 1.1 X부터는 없어졌다고 한다. 아래 최만석님의 댓글 및 다음 링크에서 "13.5.4. Proxying Classes"절을 참조한다.  이 내용에 따르면 일반 클래스도 AOP 프락시의 타겟 객체로 사용될 수 있지만, 이 방법 또한 가상 메소드만 AOP 프락시를 통해서 노출될 수 있다는 제약이 있다. )

public interface ICalculator

{

    int Add(int n1, int n2);


   //....

}


public class Calculator : ICalculator

{

    #region ICalculator Members


    public int Add(int n1, int n2)

    {

        return n1 + n2;

    }


    // ....

    #endregion

}

Calculator를 AOP의 타겟 객체로 만들고 싶다면 반드시 ICalculator같은 인터페이스를 구현 하는 구조로 설계해야 한다는 것이다. 그러나 추후 버전에서는 다른 방식의 AOP 프락시 제너레이터도 제공할 계획이란다.
앞에서 수정한 부분을 참고한다. 반드시 인터페이스를 구현할 필요는 없다.

참고로 .NET 자체에서도 AOP용 프락시를 생성하는 방법이 있다. 그러나 그런 AOP용 프락시를 생성하기 위해서는 타겟 객체가 ContextBountObject 타입을 상속받아야 한다는 단점이 있다. 컨텍스트 기반(Context-bound )의 프락시라고 한다는데, 컨텍스트 스위치와 .NET 리모팅 인프라의 오버헤드때문에 성능적으로도 그렇게 효과적이지는 않다고 한다.


■ ProxyFactoryObject를 configuration하기


Spring.NET에서는 Spring.Aop.Framework.ProxyFactoryObject 클래스를 이용해서 타겟 객체(advised 객체)에 대한 프락시를 얻는다고 했다.  이 ProxyFactoryObject를 이용하면 좋은 점은 advice나 pointcut을 configuration에 포함시킬 수 있다는 것이다. 다음 configuration을 보자.

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

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

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

3개의 객체가 정의되어 있다. 첫번째는 advice 객체에 대한 정의이다. 두번째 객체는  AOP가 적용될 타겟 객체에 대한 정의이다. 세번째 객체에 대한 정의는 첫번째와 두번째 객체에 대한 참조를 받아들여서 타겟 객체에 대한 AOP 프락시를 만들어낼 ProxyFactorObject에 대한 정의이다. 이 설정에 대한 구체적인 설명은 뒤에서 하기로 한다.

이렇게 설정한 내용을 Spring.NET의 컨테이너가 해석할 수 있다는 것이다. ProxyFactoryObject 객체에 대한 이와 같은 설정을 해석할 수 있다는 것이 무슨 의미인지는 이렇다. ProxyFactoryObject에 대한 참조를 얻는 과정은 여느 객체를 얻는 방식과 동일하다.

IApplicationContext ctx = ContextRegistry.GetContext();


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

이때 ctx.GetObject("calculatorWeaved")가 반환하는 것은 ProxyFactoryObject 객체에 대한 참조가 아니라, Spring.NET 컨테이너는 ProxytFactoryObject에 대한 요청을 인식할 수 있고 이것에 대한 요청은 다르게 처리한다. 즉 이 팩토리가 참조하고 있는 타겟 객체( Target 속성이 가리키는 타입)에 대한 프락시의 참조를 반환한다는 것이다.  이때 참조된 프락시는 설정된 advice, 여기서는 CommonLoggingAroundAdvice의 내용이 weaving된 상태이다.


여기까지 하고 다음 포스트에서 앞에서 알아본 예제를 살펴보도록 하자.