Cutting Edge

AOP(Aspect-Oriented Programming), 가로채기 및 Unity 2.0

Dino Esposito

image: Dino Esposito개체 지향은 시스템을 구성 요소로 나누고 구성 요소를 통해 프로세스를 기술하는 데 탁월하며 프로그래밍 패러다임의 주류라는 데는 의심의 여지가 없습니다. OO(개체 지향) 패러다임은 또한 구성 요소의 비즈니스 측면에도 탁월하지만, 크로스 커팅(Cross-Cutting) 문제를 해결하는 데는 그리 효과적이지 않습니다. 일반적으로 크로스 커팅(Cross-Cutting) 문제란 시스템의 여러 구성 요소에 영향을 주는 문제를 의미합니다.

클래스 계층을 디자인할 때는 복잡한 비즈니스 논리 코드를 최대한 재사용할 수 있도록 시스템의 핵심 및 주 비즈니스 기능 위주로 디자인하는 것이 일반적입니다. 그러면 클래스 계층 전체에서 사용되는 비즈니스와 관련이 없는 문제는 어떻게 해야 할까요? 즉, 캐싱, 보안 및 로깅과 같은 기능들은 어디에 넣어야 할까요? 결과적으로 영향받는 모든 개체에 이러한 기능들이 반복되는 것이 일반적입니다.

크로스 커팅(Cross-Cutting) 문제는 특정 구성 요소 또는 구성 요소 집합의 역할이 아니므로 응용 프로그램 클래스 수준을 벗어나 다른 논리적 수준에서 다루어져야 하는 시스템의 측면입니다. 이를 위해 AOP(aspect-oriented programming)라는 독특한 프로그래밍 패러다임이 고안되었습니다. 흥미롭게도 AOP의 개념은 1990년대 Xerox PARC 연구소에서 개발되었는데, 이 개발팀에서는 최초이면서 아직 가장 인기 있는 AOP 언어인 AspectJ도 개발했습니다.

거의 모든 이들이 AOP의 장점에 대해서는 공감하지만 아직 널리 구현되지는 않은 것이 사실입니다. 이렇게 보급이 저조한 데는 올바른 도구가 없는 것이 주요 원인이라고 보고 있으며, AOP가 부분적이나마 Microsoft .NET Framework에서 기본 제공되는 날이 AOP의 역사에서 분수령이 될 것입니다. 현재는 .NET에서 임시 프레임워크를 통해서만 AOP를 사용해 볼 수 있습니다.

.NET에서 가장 강력한 AOP 도구는 PostSharp이며 sharpcrafters.com에서 받을 수 있습니다. PostSharp는 AOP 이론의 모든 핵심 기능을 경험할 수 있는 전체 AOP 프레임워크를 제공합니다. 그러나 일부 AOP 기능이 포함되어 있는 DI(종속성 주입) 프레임워크도 많습니다.

예를 들어 Spring.NET, Castle Windsor, 그리고 물론 Microsoft Unity에서도 AOP 기능을 발견할 수 있습니다. 응용 프로그램 계층의 추적, 캐싱 및 장식 구성 요소와 같이 비교적 단순한 시나리오의 경우에는 일반적으로 DI 프레임워크의 기능으로 충분합니다. 그러나 도메인 개체 및 UI 개체의 경우 DI 프레임워크로는 해결이 어렵습니다. 크로스 커팅(Cross-Cutting) 문제는 분명 외부 종속성이라고 볼 수 있으며, DI 기법을 사용하면 외부 종속성을 클래스에 주입할 수 있습니다.

요점은 DI를 사용하려면 사전에 특수한 디자인이나 약간의 리팩터링이 필요하다는 것입니다. 이미 DI 프레임워크를 사용하고 있다면 여기에 몇 가지 AOP 기능을 추가하기는 어렵지 않습니다. 그러나 시스템이 DI와 연관이 없다면 DI 프레임워크를 도입하기 위해 많은 작업이 필요할 수 있습니다. 프로젝트가 대규모이거나 레거시 시스템을 업데이트하는 경우 이것이 불가능할 수 있습니다. 반면, 정식 AOP 방법에서는 모든 크로스 커팅(Cross-Cutting) 문제를 관점(aspect)이라고 하는 새로운 구성 요소로 래핑합니다. 이 기사에서는 먼저 AO(Aspect-Oriented) 패러다임을 간략하게 살펴보고 Unity 2.0에서 제공하는 AOP 관련 기능을 확인해 보겠습니다.

AOP 개요

OOP(개체 지향 프로그래밍) 프로젝트는 각기 클래스를 하나 이상 구현하는 여러 소스 파일로 구성됩니다. 프로젝트에는 또한 로깅이나 캐싱과 같은 크로스 커팅(Cross-Cutting) 문제와 관련된 클래스도 포함됩니다. 모든 클래스는 컴파일러에 의해 처리되어 실행 코드를 생성합니다. AOP에서 관점은 프로젝트 내의 여러 클래스에 요구되는 동작을 캡슐화하는 재사용 가능한 구성 요소입니다. 관점이 실제로 처리되는 방법은 여러분이 고려하는 AOP 기술에 따라 다릅니다. 일반적으로 관점은 컴파일러에 의해 간단하게 그리고 직접적으로 처리되지는 않으며, 실행 코드를 수정하여 관점을 적용하는 데는 기술에 따라 추가적인 도구가 필요합니다. 가장 먼저 개발된 AOP 도구이며 Java AOP 컴파일러인 AspectJ에서는 어떻게 구현되는지 간단하게 살펴보겠습니다.

AspectJ를 사용할 때는 Java 프로그래밍 언어로 클래스를 작성하고 AspectJ 언어로 관점을 작성합니다. AspectJ에서는 관점의 원하는 동작을 지정할 수 있는 사용자 지정 구문을 지원합니다. 예를 들어 로깅 관점은 특정 메서드가 실행되기 전과 후에 로그하도록 지정할 수 있습니다. 관점은 특정한 방법으로 일반 소스 코드에 병합되고 중간 버전의 소스 코드를 생성하며, 이어 실행 가능한 형식으로 컴파일됩니다. AspectJ 용어로 관점을 전처리하고 소스 코드와 병합하는 구성 요소를 위버(weaver) 라고 합니다. 위버는 컴파일러가 실행 파일로 만들 수 있는 출력을 생성합니다.

요약하자면 관점은 기존 클래스에 주입하려는 재사용 가능한 코드 조각을 이러한 클래스의 소스 코드 수정 없이 기술하는 방법입니다. .NET PostSharp와 같은 다른 AOP 프레임워크에는 위버 도구가 없으며, 관점의 내용이 프레임워크에 의해 처리되고 결과적으로 일종의 코드 주입이 수행됩니다.

이러한 면에서 코드 주입은 종속성 주입과는 차이가 있습니다. 코드 주입은 주어진 관점으로 특성이 지정된 클래스 본문의 특정 지점에 관점의 공용 끝점에 대한 호출을 주입할 수 있는 AOP 프레임워크의 기능을 의미합니다. PostSharp 프레임워크를 예로 들면 관점을 클래스의 메서드에 연결할 수 있는 .NET 특성으로 작성할 수 있습니다. PostSharp 특성은 빌드 후 단계에서 PostSharp 컴파일러(이 역시 위버라고 할 수 있음)에 의해 처리됩니다. 최종적으로 여러분의 코드는 특성의 코드 일부를 포함하도록 향상됩니다. 그러나 주입 지점은 자동으로 확인되며 개발자가 할 일은 자체 포함 관점 구성 요소를 작성하고 이를 공용 클래스 메서드에 연결하는 것이 전부입니다. 작성하기 쉬운 것은 물론 코드를 유지 관리하기는 더 쉽습니다.

몇 가지 세부적인 용어를 소개하고 그 의미를 설명하면서 AOP에 대한 개요를 마치겠습니다. 조인 지점(join point) 은 관점의 코드를 주입하려는 대상 클래스 소스 코드 내의 지점을 나타냅니다. 포인트컷(pointcut) 은 조인 지점의 컬렉션을 나타냅니다. 어드바이스(advice) 는 대상 클래스에 주입할 코드를 나타냅니다. 코드는 조인 지점 앞, 뒤, 그리고 주변에 주입할 수 있습니다. 어드바이스는 포인트컷과 연결됩니다. 이러한 용어는 AOP의 원래 정의에서 설명된 것이며 현재 사용하고 있는 특정 AOP 프레임워크에는 문자 그대로 적용되지 않았을 수 있습니다. AOP의 기반이라고 할 수 있는 이러한 용어의 개념을 이해하고 이러한 지식을 바탕으로 특정 프레임워크에 대한 세부적인 내용을 이해하는 것이 좋습니다.

Unity 2.0 개요

Unity는 Microsoft Enterprise Library 프로젝트의 일부로 제공되는 응용 프로그램 블록이며, 별도로 다운로드할 수도 있습니다. Microsoft Enterprise Library는 로깅, 캐싱, 암호화, 예외 처리 등과 같이 .NET 응용 프로그램 개발의 특성을 잘 나타내는 다양한 크로스 커팅(Cross-Cutting) 문제를 해결하는 응용 프로그램 블록의 컬렉션입니다. 2010년 4월에 출시된 최신 버전인 Enterprise Library 5.0에는 Visual Studio 2010에 대한 지원을 완벽하게 제공합니다. 자세한 내용은 패턴 및 실습 개발자 센터(msdn.microsoft.com/library/ff632023)를 참조하십시오.

Unity는 Enterprise Library 응용 프로그램 블록 중 하나입니다. Unity는 Silverlight용으로도 제공되며 클래스를 더 관점 지향적으로 만들기 위한 가로채기 메커니즘을 추가적으로 지원하는 DI 컨테이너라고 할 수 있습니다.

Unity 2.0의 가로채기

Unity의 가로채기의 핵심 개념은 개발자가 개체의 메서드를 호출하기 위해 수행하는 호출 체인을 사용자 지정할 수 있도록 하는 것입니다. 즉, Unity 가로채기 메커니즘은 구성된 개체에 대해 수행되는 호출을 캡처하고 일반적인 메서드 실행의 앞, 뒤, 그리고 주변에 약간의 부수적 코드를 추가하여 대상 개체의 동작을 사용자 지정합니다. 가로채기는 개체의 소스 코드를 변경하거나 동일한 상속 경로에 있는 클래스의 동작에 영향을 주지 않고 런타임에 개체에 새 동작을 추가할 수 있는 매우 유연한 방법입니다. Unity 가로채기는 런타임에 개체가 사용되는 동안 개체의 기능을 확장하기 위해 고안된 인기 있는 디자인 패턴인 Decorator 패턴을 구현하기 위한 방법입니다. Decorator는 대상 개체의 인스턴스를 받고 이에 대한 참조를 유지하며 외부 환경으로 해당 기능을 보강하는 컨테이너 개체입니다.

Unity 2.0의 가로채기 메커니즘은 인스턴스 및 형식 가로채기를 모두 지원합니다. 또한 가로채기는 개체가 인스턴스화된 방법, 개체가 Unity 컨테이너를 통해 생성되었는지 여부 또는 알려진 인스턴스인지 여부에 관계없이 작동합니다. 후자의 경우에는 다른 완전한 독립 실행형 API를 사용할 수 있습니다. 그러나 이렇게 하면 구성 파일 지원을 사용할 수 없게 됩니다. 그림 1에는 Unity에서 가로채기 기능의 아키텍처가 나와 있으며, 컨테이너를 통해 확인되지 않은 특정 개체 인스턴스에서 이 기능이 작동하는 방법을 보여 줍니다. 이 그림은 MSDN 설명서에 나오는 그림을 약간 수정한 버전입니다.

image: Object Interception at Work in Unity 2.0

그림 1 Unity 2.0에서 개체 가로채기가 작동하는 방법

가로채기 하위 시스템은 인터셉터(또는 프록시), 동작 파이프라인, 그리고 동작 또는 관점이라는 세 가지 핵심 요소로 이루어져 있습니다. 하위 시스템의 양쪽 끝에는 클라이언트 응용 프로그램과 대상 개체가 있습니다. 즉, 추가 동작이 할당되는 개체의 소스 코드에 동작이 하드 코드되지 않습니다. 지정한 인스턴스에 Unity의 가로채기 API를 사용하도록 클라이언트 응용 프로그램을 구성하면 모든 메서드 호출이 프록시 개체, 즉 인터셉터를 통해 수행됩니다. 이 프록시 개체는 등록된 동작의 목록을 조회하고 내부 파이프라인을 통해 동작을 호출합니다. 구성된 각 동작에는 개체 메서드의 일반적인 호출 전 또는 후에 실행할 수 있는 기회가 주어집니다. 프록시는 입력 데이터를 파이프라인으로 주입하고 대상 개체가 처음에 생성한 반환 값을 받으며 추가로 동작을 수정합니다.

가로채기 구성

Unity 2.0에서 가로채기 사용을 위한 권장되는 방법은 이전 버전과는 다르지만 이전 버전에서 사용되던 방법 역시 호환성을 위해 완전하게 지원됩니다. Unity 2.0에서 가로채기는 개체가 확인되는 방법을 기술하기 위해 컨테이너에 추가하는 새로운 확장입니다. 가로채기를 유연한 코드로 구성하려면 코드를 다음과 같이 작성하면 됩니다.

var container = new UnityContainer();
container.AddNewExtension<Interception>();

컨테이너는 가로챌 형식과 추가할 동작에 대한 정보를 찾아야 하는데 이 정보는 유연한 코드를 사용하거나 구성을 통해 추가할 수 있습니다. 구성을 사용하면 응용 프로그램을 수정하거나 새로운 컴파일 단계를 추가하지 않아도 마음대로 수정할 수 있어 매우 유연합니다. 여기에서는 구성 기반 방법을 사용해 보겠습니다.

먼저 구성 파일에 다음과 같은 내용을 추가합니다.

<sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.
  Configuration.InterceptionConfigurationExtension, 
  Microsoft.Practices.Unity.Interception.Configuration"/>

이 스크립트의 목적은 구성 스키마를 가로채기 하위 시스템과 연관된 새로운 요소와 별칭으로 확장하는 것입니다. 다음과 같은 항목도 추가해야 합니다.

<container> 
  <extension type="Interception" /> 
  <register type="IBankAccount" mapTo="BankAccount"> 
    <interceptor type="InterfaceInterceptor" /> 
    <interceptionBehavior type="TraceBehavior" /> 
  </register> 
</container>

유연한 코드를 사용해서 동일한 효과를 얻으려면 컨테이너 개체에서 AddNewExtension<T> 및 RegisterType<T>을 호출하면 됩니다.

이제 구성 스크립트를 더 자세하게 살펴보겠습니다. <extension> 요소는 컨테이너에 가로채기를 추가합니다. 스크립트에서 사용된 “Interception”은 extension 섹션에 정의된 별칭 중 하나입니다. 인터페이스 형식 IBankAccount는 구체적인 형식 BankAccount(이것이 DI 컨테이너의 기존 작업입니다)와 매핑되며 인터셉터의 특정 형식과 연결됩니다. Unity는 두 가지 주 인터셉터 형식으로 인스턴스 인터셉터와 형식 인터셉터를 제공합니다. 다음 달에는 인터셉터에 대해 더 자세하게 알아보겠습니다. 여기에서 인스턴스 인터셉터는 가로채는 인스턴스에 대한 들어오는 호출을 필터링하는 프록시를 만든다는 것만 알아두면 됩니다. 형식 인터셉터는 가로채는 개체의 형식을 모의 개체로 만들고 파생된 형식의 인스턴스에서 작업을 수행합니다. 인터셉터에 대한 자세한 내용은 msdn.microsoft.com/library/ff660861(PandP.20)을 참조하십시오.

인터페이스 인터셉터는 개체의 한 인터페이스의 프록시로만 작동할 수 있도록 제한된 인스턴스 인터셉터입니다. 인터페이스 인터셉터는 프록시 클래스를 생성하기 위해 동적 코드 생성을 사용합니다. 구성의 가로채기 동작 요소는 가로챈 개체 인스턴스 주변에서 실행하려는 외부 코드를 나타냅니다. 클래스 TraceBehavior는 클래스와 해당 종속성을 컨테이너가 확인할 수 있도록 선언적으로 구성됩니다. 클래스와 예상되는 해당 생성자에 대한 내용을 컨테이너에 전달하는 데는 다음과 같이 <register> 요소를 사용합니다.

<register type="TraceBehavior"> 
   <constructor> 
     <param name="source" dependencyName="interception" /> 
   </constructor> 
</register>

그림 2는 TraceBehavior 클래스의 일부를 보여 줍니다.

그림 2 샘플 Unity 동작

class TraceBehavior : IInterceptionBehavior, IDisposable
{
  private TraceSource source;

  public TraceBehavior(TraceSource source)
  {
    if (source == null) 
      throw new ArgumentNullException("source");

    this.source = source;
  }
   
  public IEnumerable<Type> GetRequiredInterfaces()
  {
    return Type.EmptyTypes;
  }

  public IMethodReturn Invoke(IMethodInvocation input, 
    GetNextInterceptionBehaviorDelegate getNext)
  {
     // BEFORE the target method execution 
     this.source.TraceInformation("Invoking {0}",
       input.MethodBase.ToString());

     // Yield to the next module in the pipeline
     var methodReturn = getNext().Invoke(input, getNext);

     // AFTER the target method execution 
     if (methodReturn.Exception == null)
     {
       this.source.TraceInformation("Successfully finished {0}",
         input.MethodBase.ToString());
     }
     else
     {
       this.source.TraceInformation(
         "Finished {0} with exception {1}: {2}",
         input.MethodBase.ToString(),
         methodReturn.Exception.GetType().Name,
         methodReturn.Exception.Message);
     }

     this.source.Flush();
     return methodReturn;
   }

   public bool WillExecute
   {
     get { return true; }
   }

   public void Dispose()
   {
     this.source.Close();
   }
 }

동작 클래스는 기본적으로 Invoke 메서드로 구성된 IInterceptionBehavior를 구현합니다. Invoke 메서드에는 인터셉터의 제어 하에 있는 메서드에 사용하려는 전체 논리가 포함되어 있습니다. 대상 메서드가 호출되기 전에 어떤 작업을 하려면 메서드 시작 부분에 하면 됩니다. 대상 개체에 양보하려면, 더 정확하게 말해 파이프라인에 등록된 다음 동작에 양보하려면 프레임워크에서 제공되는 getNext 대리자를 호출하면 됩니다. 마지막으로 원하는 임의의 코드로 대상 개체를 후처리할 수 있습니다. Invoke 메서드는 파이프라인의 다음 요소에 대한 참조를 반환해야 하며 null이 반환되면 체인이 중단되고 추가 동작이 호출되지 않습니다.

구성 유연성

가로채기, 그리고 더 넓은 의미로 AOP는 여러 흥미로운 시나리오를 해결할 수 있는 열쇠입니다. 예를 들어 가로채기를 사용하면 전체 클래스를 수정하지 않고 개별 개체에 역할을 추가할 수 있으므로 Decorator를 사용할 때보다 솔루션을 훨씬 유연하게 유지할 수 있습니다.

이 기사에서는 .NET에 적용된 AOP에 대해 간략하게 살펴보았습니다. 앞으로 몇 개월 동안 Unity와 AOP에 대한 전반적인 내용을 더 다룰 것입니다.

Dino Esposito 는 “Programming Microsoft ASP.NET MVC”, Microsoft Press(2010)의 저자이며 “Microsoft .NET: Architecting Applications for the Enterprise”, (Microsoft Press, 2008)의 공동 저자입니다. 이탈리아에 거주하고 있는 Esposito는 전 세계의 IT 업계 관련 행사에서 많은 활동을 펼치고 있습니다. 문의 사항이 있으면 블로그 weblogs.asp.net/despos를 방문하시기 바랍니다.

이 문서를 검토하는 데 많은 도움을 주신 기술 전문가인 Chris Tavares에게 감사 인사를 전합니다.