'2018/08'에 해당되는 글 2건

  1. 2018.08.28 AOP 프레임워크 이해와 개발
  2. 2018.08.27 [마이크로서비스] 아키텍처 구축하기


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 를 구현해 봄으로서 언어적 특성을 더 잘 알 수 있고, 언어가 제공하는 플랫폼을 이해하는 데 큰 도움이 될 것이다.

Posted by 땡초 POWERUMC

댓글을 달아 주세요

[마이크로서비스] 아키텍처 구축하기

마이크로서비스는 서비스를 작게 나누는 것부터 시작한다. 이렇게 서비스를 작게 나누게 되면 여러가지 장점이 있는데 [이 글]에서 마이크로서비스 아키텍처를 이해할 수 있다. 가볍고 탄력적인 서비스를 구축이 필요한 곳에서 이 아키텍처를 도입하고 있다.

물론 장점만 있는 것은 아니다. 그 중에서 단연 러닝커브가 높은 기술적 구현 측면과 요구사항이 바로 그것이다. 아시다시피 서비스가 작게 나눠지면서 개발/네트워킹/보안/배포/모니터링에 이르기까지 해결해야 할 과제가 생긴다.

이 아티클은 개발 영역에 초점을 맞추어 예제 코드가 작성 되었다. 개발 영역을 마이크로서비스화 할 때 적지 않은 부분의 변화가 필요한데, 그 기법과 라이브러리 등도 함께 살펴보면 도움이 될 것 같다.

[https://github.com/powerumc/microservice-architecture-quick-start]

위 소스 코드는 마이크로서비스의 기술적인 측면을 강조하여 예제가 작성이 되었고, 아직 구현되지 않은 부분도 있으니 아래의 체크리스트를 참고하자.

인프라스트럭쳐

오케스트레이션

  • [ ] Kubernetes
    잘 알려진 오케스트레이션 도구인 쿠버네티스다. Yaml 파일을 만들어 제공되지만, 쿠버네티스 환경에서 구동하려면 마스터(master)와 노드(nodes) 머신이 구성되어야 하고, 깃헙으로 예제를 제공하기엔 적절하지 않다고 판단했다. 추후에 기회가 되면 [vagrant] 환경과 [ansible] 을 적용한 코드를 예제로 제공할 예정이다. (그게 언제가 될지…)

  • [x] Docker
    
잘 알려진 컨테이너 기술이며 가능하다면 이 도구를 필히 마스터 할 필요가 있다.

  • [x] Docker-Compose
    
여러 개의 Docker 컨테이너를 띄우기 위한 도구이다.

인프라스트럭쳐

  • [ ] Ocelot
    
ASP.NET Core 기술로 개발된 API Gateway 이다. 작게 쪼개진 API 서비스를 개발하다 보면 서로 종속적인 관계가 발생할 수 있는데, 그런 관계를 하나의 API Gateway 를 통과하게 함으로써 관리적으로 용이하다. API 관리와 함께 접근제어, 인증/권한, 로깅, 트래픽 제어 등의 역할을 수행한다.

  • [x] NLog
    로깅 라이브러리이다.

  • [x] Swagger Integration (Swashbuckle) 
 코드로 정의한 API 를 문서화 하는 도구이다.

  • [x] Entity Framework Core Code First
    자바의 JPA 의 닷넷 버전이다. 자바의 JPA 가 세상에 나오기 훨씬 전부터 마이크로소프트 주도하에 개발되어온 프레임워크인데, JPA 비해 훨씬 사용성이 좋다. 물론 JPA 만큼 고급 옵션이 준비되어 있지는 않지만, 어지간한 비즈니스 플로우는 모두 구현할 수 있다.

  • [x] TraceId about Request
    필자가 개발한 ASP.NET Core 미들웨어이다. 마이크로서비스는 서비스간에 트랜잭션의 추적이 필요한데, 그 트랜잭션을 추적하기 위한 용도로 개발되었다.

  • [x] Guard
    간단한 코드 스니펫이다. 보통 new 키워드를 통해 Exception 을 발생하는데, Guard 라는 static class 를 이용하여 인자와 로직을 검증하게 만들었다. 더 고급 개발자라면 [Jetbrains.Annotations] 라이브러리를 이용하는 것도 좋은 선택이다.

  • [ ] AutoMapper
    이 라이브러리에는 호출호가 참 많다. 그래서 사용하지 않았는데 앞으로도 사용할 일은 거의 없을 것 같다.

  • [x] Data Protection
    먼저 MSDN 의 [이 문서]를 참고해 보면 좋다. 말도 안되게 기계 번역된 페이지를 보면 현기증이 나지만 그래도 끝까지 한번 읽어보자. 결론만 말하자면 개발된 웹 서비스를 분산하고 특정 기능(ASP.NET Core Session 과 같은) 을 사용하려면 데이터 보호 기능을 사용해야 한다. 가령, 웹 서비스를 분산하기 위해 분산 세션 기능을 사용하게 되면 ASP.NET Core 내부적으로 암호화 작업을 하게 되는데 이 때 데이터 보호 기능이 필요하다.

  • [x] Polly
    마이크로서비스는 항상 작동하지 않을 수 있다라는 전제가 필요하다. 서비스가 뻗을 수도 있고 응답이 느릴 수도 있다. 이 때 정책적으로 여러 번 재호출 하거나, 일정한 간격을 두고 재시도 하는 등의 정책을 설정할 때 필요하다.

도메인 기반 개발

  • [x] Aggregate Root
    하나의 웹 서비스의 도메인을 분류 하였다면 도메인의 루트가 있을텐데, 이 루트 개체를 총칭하는 용어이다.

  • [ ] ValueObject
    DDD 에서는 고유 ID 를 가지지 않는 불변객체를 지칭하는 용어이다.

  • [x] CQRS
    필자가 설명하긴 여전히 스터디 단계이므로, 이규원님 블로그의 [이 아티클]을 참고하면 잘 설명되어 있다.

  • [x] Event Sourcing
    위 아이클 참고.

  • [x] Event Bus
    이벤트 버스는 이벤트가 발생하면 특정 대상으로 이벤트롤 전달하는 패턴을 말한다. 일반적으로 구독/발행 패턴의 구현체가 되는데, 이 패턴을 사용하게 되면 복잡성을 피하고, 컴포넌트간에 커뮤니케이션을 간단하게 구현할 수 있다.

  • [ ] EventBus by RabbitMq
    이벤트가 발생하면 RabbitMq 큐(Queue) 로 전달하는 기능이다.

  • [ ] Unit Of Work
    마틴 파울로의 [이 글]을 참고한다. 마틴 파울러는 이 패턴은 비즈니스 트랜잭션을 비즈니스 트랜잭션을 관리하고, 동시성 문제를 해결한다고 정의하였다. 일종의 Dispatcher 역할을 한다고 볼 수 있다.

모니터링

  • [x] Health Check
    기본적으로 서버가 살아있는지 상태를 알기 위한 API 이다.

  • [x] App.Metrics
    서버의 행동/행위를 측정하기 위한 라이브러리이다.

  • [ ] Grafana
    
App.Metrics 의 측정 항목을 가시화하기 위한 도구이다.



Posted by 땡초 POWERUMC

댓글을 달아 주세요