티스토리 뷰


AOP 프레임워크 이해와 개발

AOP(Aspect Oriented Programming), 관점지향 프로그래밍은 OOP(Object Oriented Programming) 에게 ‘관심사’라는 관점을 더해 객체 지향 프로그래밍의 변경 없이 다른 관점의 구현을 추가할 수 있다. 더 쉽게 말하면 클래스나 메서드가 동작할 때 코드의 변경 없이 원하는 동작을 추가하는 기법이다.

흔히 AOP 의 예를 들때 ‘로깅(Logging)’ 을 든다. 기존 코드의 변경 없이 코드 본문이 실행 되기전 매개변수 값 등을 로깅하도록 하는 것이다. 물론 로깅 이외에 다양한 용도로 사용되는데, 비즈니스 로직의 검증이나 응용 프로그램 전역적으로 공통적인 관심사 분리에 사용된다.

AOP 프로그래밍의 활용 예

  • 로깅
  • 유효성 검사
  • 트랜잭션 처리
  • 인증/보안 등

AOP 구현 방법

AOP 프레임워크 개발은 비교적 고급 기술에 속한다. AOP 를 이해하고 쓰는 사람들은 많지만 내부 구현까지 이해하는 사람은 드문 것이 사실이다. 그리고 내부 구현을 이해해도 직접 만드는 것은 또 다른 이야기일 것이다.

필자는 어려운 이야기일 수 있는 이 부분에 대해 언급하고자 한다. AOP 프레임워크는 구현 방법이 매우 다양한데, 크게 두 가지 방법으로 요약할 수 있다. (더 자세한 분류는 이 링크를 참고)

AOP 프레임워크 구현 방안

  1. 런타임(Runtime) 구동 방식
    흔히 Dynamic Proxy 라고 하는데, 동적으로 프록시 패턴을 구현하는 객체를 생성해 내는 기법이다. 메모리에 직접 인스트럭션(Instruction) 을 쓰는데 언어마다, 그리고 컴파일 옵션에 따라 인스트럭션이 다를 수 있다. 따라서 최적화에 따라 성능을 좌지우지 한다.
    C# 에서는 MSIL(Microsoft Intermediate Language), 자바는 바이트코드(Bytecode), C/C++은 어셈블리(Assembly) 코드를 메모리에 생성하는 방식이다.
  2. 빌드 타임(Build-time) 구동 방식
    이 방식은 빌드 프로세스를 지원하는 언어에서 가능한 방식이다. 실제 작성한 코드를 컴파일 하기 전에 AOP 의 위빙(Weaving) 코드를 삽입하고 나중에 컴파일 하는 방식을 말한다. C# 에서는 PostSharp, 자바에는 AspectJ 가 대표적이고, 대체적으로 런타임 구동 방식에 비해 성능이 좋다.

AOP 구현

우선 필자가 간단하게 만든 SimpleAop 라이브러리를 참고해 보는 것이 좋겠다. 예전에 만든 이 링크 참고하면 많은 도움이 될 것 같다. 그리고 자바스크립트로 구현한 Javascript OOP-AOP-IoC 도 참고하면 좋다.

프로그래밍이란 무엇인가?

오로지 CPU 입장에서 본다면 프로그래밍은 이미 정의된 함수를 어떻게 호출할 것인가로 귀결된다. CPU 아키텍처에 따라 다르지만 대부분 함수 매개변수는 스택의 로드(Load)와 (Push) 로 구현된다. 그리고 매개변수가 적재되면 Call 인스트럭션을 보내 함수를 호출하는 것이다.

물론 모든 언어가 이에 해당하는 것은 아니다. 아름다움을 추구하는 오브젝티브-C 언어 1/ 2- 언어적 특성 은 조금은 다른 메커니즘으로 동작한다.

구현

첫 번째, 모든 함수는 return 인스트럭션을 가진다. 흔히 void 함수는 return 이 없어도 되지만 컴파일 된 코드(MSIL, Bytecode, Assembly 등)은 함수의 마지막은 항상 return 으로 종료된다. return 의 의미는 함수를 종료하는 것이 아니라 나를 호출한 caller 에게 되돌아 가라는 의미이다.

두 번째, 객체 지향 프로그래밍에서 상속한 클래스의 생성자는 항상 부모 객체를 먼저 생성한다. 다음의 코드를 보면 조금은 더 이해하기 쉬울 것이다.

foreach (var constructor in _implementationType.GetConstructors())
{
	var constructorTypes = constructor.GetParameters().Select(o => o.ParameterType).ToArray();
	var c = _typeBuilder.DefineConstructor(
		MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.RTSpecialName | MethodAttributes.SpecialName,
		CallingConventions.Standard,
		constructorTypes);
	
	var il = c.GetILGenerator();
	il.Emit(OpCodes.Ldarg_0);

	for (var i = 0; i < constructorTypes.Length; i++)
	{
		il.Emit(OpCodes.Ldarg, i + 1);
	}

	il.Call(constructor);
	il.Emit(OpCodes.Nop);
	il.Emit(OpCodes.Ret);
}

세 번째, SimpleAop 에서는 런타임에 프록시 패턴을 구현하는 방식으로 AOP 를 구현하였다. 프록시 패턴을 런타임에 구현하기 위해서는 원본 대상이 필요한데, 이는 인터페이스(Interface) 정의를 사용한다. 대부분의 테스팅 프레임워크의 Mock 객체들은 인터페이스가 필요한데, 바로 프록시를 생성하기 위한 대상으로 사용되기 때문이다.

특히 자바에서는 기본적으로 virtual 메서드이기 때문에, 런타임에 클래스는 override 하기 용이하다. 반면 C# 언어는 virtual 메서드가 아니기 때문에, virtual 로 선언된 대상 클래스가 필요할 수도 있다. 이는 AOP 프레임워크 마다 구현 방법도 다르기 때문에 사용할 AOP 프레임워크가 어떤 방식인지 알아두면 좋을 것이다.

foreach (var method in _interfaceType.GetMethods(BindingFlags.Public | BindingFlags.Instance))
{
    var methodTypes = method.GetParameters().Select(o => o.ParameterType).ToArray();
    var m = _typeBuilder.DefineMethod($"{method.Name}",
        MethodAttributes.Private | MethodAttributes.Final | MethodAttributes.HideBySig |
        MethodAttributes.Virtual | MethodAttributes.NewSlot,
        CallingConventions.HasThis,
        method.ReturnType,
        methodTypes);
    
    _typeBuilder.DefineMethodOverride(m, _interfaceType.GetMethod(method.Name, methodTypes));
    
    var il = m.GetILGenerator();
    var localReturnValue = il.DeclareReturnValue(method);
    
    var localCurrentMethod = il.DeclareLocal(typeof(MethodBase));
    var localParameters = il.DeclareLocal(typeof(object[]));
    
    // var currentMethod = MethodBase.GetCurrentMethod();
    il.Call(typeof(MethodBase).GetMethod(nameof(MethodBase.GetCurrentMethod)));
    il.Emit(OpCodes.Stloc, localCurrentMethod);
    
    // var baseMethod = method.BaseType.GetMethod(...);
    var localBaseMethod = il.DeclareLocal(typeof(MethodBase));
    il.Emit(OpCodes.Ldloc, localCurrentMethod);
    il.Call(typeof(OnMethodBoundAspectAttributeExtension).GetMethod(nameof(OnMethodBoundAspectAttributeExtension.GetCustomAttributeOnBaseMethod)));
    il.Emit(OpCodes.Stloc, localBaseMethod);
    
    
    // var parameters = new[] {a, b, c};
    il.Emit(OpCodes.Ldc_I4, methodTypes.Length);
    il.Emit(OpCodes.Newarr, typeof(object));
    if (methodTypes.Length > 0)
    {
        for (var i = 0; i < methodTypes.Length; i++)
        {
            il.Emit(OpCodes.Dup);
            il.Emit(OpCodes.Ldc_I4, i);
            il.Emit(OpCodes.Ldarg, i + 1);
            if (methodTypes[i].IsValueType)
            {
                il.Emit(OpCodes.Box, methodTypes[i].UnderlyingSystemType);
            }

            il.Emit(OpCodes.Stelem_Ref);
        }
    }
    il.Emit(OpCodes.Stloc, localParameters);

    // var aspectInvocation = new AspectInvocation(method, this, parameters);
    var localAspectInvocation = il.DeclareLocal(typeof(AspectInvocation));
    il.Emit(OpCodes.Ldloc, localCurrentMethod);
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldloc, localParameters);

    il.New(typeof(AspectInvocation).GetConstructors()[0]);
    il.Emit(OpCodes.Stloc, localAspectInvocation);
    
    // var classAttributes = GetType().GetOnMethodBoundAspectAttributes();
    var localClassAttributes = il.DeclareLocal(typeof(OnMethodBoundAspectAttribute[]));
    il.Emit(OpCodes.Ldarg_0);
    il.Call(_implementationType.GetMethod(nameof(GetType)));
    il.Call(typeof(OnMethodBoundAspectAttributeExtension).GetMethod(nameof(OnMethodBoundAspectAttributeExtension.GetOnMethodBoundAspectAttributes), new[] {typeof(Type)}));
    il.Emit(OpCodes.Stloc, localClassAttributes);
    
    // var methodAttributes = method.GetOnMethodBoundAspectAttributes();
    var localMethodAttributes = il.DeclareLocal(typeof(OnMethodBoundAspectAttribute[]));
    il.Emit(OpCodes.Ldloc, localBaseMethod);
    il.Call(typeof(OnMethodBoundAspectAttributeExtension).GetMethod(nameof(OnMethodBoundAspectAttributeExtension.GetOnMethodBoundAspectAttributes), new[] {typeof(MethodBase)}));
    il.Emit(OpCodes.Stloc_S, localMethodAttributes);
    
    
    // classAttributes.ForEachOnBefore(invocation);
    il.Emit(OpCodes.Ldloc, localClassAttributes);
    il.Emit(OpCodes.Ldloc, localAspectInvocation);
    il.Call(typeof(OnMethodBoundAspectAttributeExtension).GetMethod(nameof(OnMethodBoundAspectAttributeExtension.ForEachOnBefore)));
    il.Emit(OpCodes.Nop);
    
    // methodAttributes.ForEachOnBefore(invocation);
    il.Emit(OpCodes.Ldloc, localMethodAttributes);
    il.Emit(OpCodes.Ldloc, localAspectInvocation);
    il.Call(typeof(OnMethodBoundAspectAttributeExtension).GetMethod(nameof(OnMethodBoundAspectAttributeExtension.ForEachOnBefore)));
    il.Emit(OpCodes.Nop);
    
    il.LoadParameters(method);
    il.Call(_implementationType.GetMethod(method.Name, methodTypes));
    
    // methodAttributes.ForEachOnAfter(invocation);
    il.Emit(OpCodes.Ldloc, localMethodAttributes);
    il.Emit(OpCodes.Ldloc, localAspectInvocation);
    il.Call(typeof(OnMethodBoundAspectAttributeExtension).GetMethod(nameof(OnMethodBoundAspectAttributeExtension.ForEachOnAfter)));
    il.Emit(OpCodes.Nop);
    
    // classAttributes.ForEachOnAfter(invocation);
    il.Emit(OpCodes.Ldloc, localClassAttributes);
    il.Emit(OpCodes.Ldloc, localAspectInvocation);
    il.Call(typeof(OnMethodBoundAspectAttributeExtension).GetMethod(nameof(OnMethodBoundAspectAttributeExtension.ForEachOnAfter)));
    il.Emit(OpCodes.Nop);
    
    il.Return(method, localReturnValue);
}

정리

기본적인 구현과 코드로 AOP 를 구현하는 방법에 대해 알아보았다. 간단하게 AOP 를 사용하는 코드를 살펴보면 다음과 같다.

[LoggingAspect]
public class Print : IPrint
{
    public void PrintMessage(string message)
    {
        Console.WriteLine(message);
    }
}

// --- Before: Void PrintMessage(System.String), 9a19fdd7e64943c9b22ae2c79a886b50, Hello World ---
// Hello World
// --- After ---

SimpleAop 코드를 보면 알겠지만, 비교적 짧은 코드로 AOP 프레임워크(?) 를 구현하였다. 이미 이보다 좋은 AOP 프레임워크가 많이 있겠지만, 직접 AOP 를 구현해 봄으로서 언어적 특성을 더 잘 알 수 있고, 언어가 제공하는 플랫폼을 이해하는 데 큰 도움이 될 것이다.

댓글