.NET AOP DynamicProxy

2023. 6. 5. 22:35Language/C#

최근 ASP.NET으로 백엔드를 개발 중입니다.

Transaction 관리 부분을 어떻게 하면 좀 더 쉽게 만들 수 있을까 고민하던 중 DynamicProxy를 알게 되어 정리하려 합니다.

현재 상황

지금 만들고 있는 백엔드는 계층형 아키텍처를 사용 중이며, EF Core를 사용하면, 쉽게 트랜젝션이 관리 된다고 하던데 저희는 Oracle과 Dapper를 사용하기 때문에 트랜젝션을 Business 계층에서 관리하려 합니다.

public class UserService : IUserService
{
    private IUserRepo userRepo;
    private ICircleRepo circleRepo;

    public UserService(IUserRepo userRepo, ICircleRepo circleRepo)
    {
        userRepo = userRepo;
        circleRepo = circleRepo;
    }

    public async Task<IEnumerable<Users>> AddUsersAndNewCircle(IEnumerable<UserDto> users, CircleDto newCircle)
    {
        // 새로운 Circle 등록
        // Uses를 새로운 Circle에 등록
        // Circle에 포함 된 Users 반환
    }
}

위와 같이 Service에는 여러 Repository들이 있고 트랜젝션을 관리해야 합니다.

AOP

AOP는 Aspect Oriented Programming으로 관점 지향 프로그래밍이라고 불립니다. 쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누고, 그 관점을 기준으로 각각 모듈화하는 기법입니다.

위 그림과 같이 공통된 로직을 한곳으로 모으는 것을 AOP라고 합니다.

예를 들어 여러 메서드들이 있는데, 실행 전/후로 로그를 찍어야 한다고 해봅시다.

그럼 모든 메서드들에 중복된 코드가 많아지고 메인 비즈니스가 잘 보이지 않게 됩니다.

그래서 Proxy 패턴이나 Decorator 패턴으로 이를 해결합니다.

해결책 DispatchProxy 사용

먼저 MS에서 기본적으로 제공해주는 DispatchProxy를 가지고 시도해봤습니다. DispatchProxy는 .NET Core 부터 추가 된 기능으로 Reflection을 사용해서 메서드를 실행 시킬 수 있습니다.

Attribute 추가

    [Transaction]
    public async Task<IEnumerable<Users>> AddUsersAndNewCircle(IEnumerable<UserDto> users, CircleDto newCircle)
    {
        // 새로운 Circle 등록
        // Uses를 새로운 Circle에 등록
        // Circle에 포함 된 Users 반환
    }

위와 같이 트랜젝션이 필요한 메서드만 사용하기 위해서 Attribute를 따로 생성했습니다.

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class TransactionAttribute : Attribute, IAspectAttibute
{
    private TransactionScope _scope { set; get; } = default!;
    private TransactionOptions _option { set; get; } = default!;

    public TransactionAttribute(IsolationLevel isolationLevel = IsolationLevel.Serializable, long secondstimeout = 30)
    {
        _option = new TransactionOptions
        {
            IsolationLevel = isolationLevel,
            Timeout = new TimeSpan(secondstimeout * 10000000L)
        };
    }

    public bool OnAspectBefore<T>(MethodInfo? targetMethod, object?[]? args)
    {
        _scope = new TransactionScope(TransactionScopeOption.Required, _option);
        return true;
    }

    public bool OnAspectAfter<T>(MethodInfo? targetMethod, object?[]? args)
    {
        _scope.Complete();
        _scope.Dispose();
        return true;
    }
}

DispatchProxy 추가

Invoke() 부분에서 Attribute를 검사하고 Before에서 트랜젝션 scope를 생성 후 After에서 Commit을 하게 됩니다.

public class ProxyDispatch<T> : DispatchProxy
{
    public T? Tartget { set; private get; } = default!;
    public IAspectAttibute? Attibute { set; private get; } = default!;

    protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
    {
        if (Attibute == null) throw new ArgumentNullException(nameof(Attibute));
        else if (targetMethod == null) throw new ArgumentNullException(nameof(targetMethod));


        var hasAttribute = targetMethod!.GetCustomAttributes(true).Any(a => a.GetType().GetInterfaces().Any(k => k == typeof(IAspectAttibute)));

        if (!hasAttribute) return targetMethod!.Invoke(Tartget, args);

        try
        {
            Before(targetMethod, args);
            var invoke = targetMethod!.Invoke(Tartget, args);
            After(targetMethod, args);
            return invoke;
        }
        catch
        {
            throw;
        }
    }

    private void Before(MethodInfo? targetMethod, object?[]? args)
    {
        var afterResult = Attibute!.OnAspectBefore<T>(targetMethod, args);
        if (afterResult == false) throw new SimpleProxyException("");
    }

    private void After(MethodInfo? targetMethod, object?[]? args)
    {
        var afterResult = Attibute!.OnAspectAfter<T>(targetMethod, args);
        if (afterResult == false) throw new SimpleProxyException("");
    }
}

문제점

Stackoverflow Reflection MethodInfo.Invoke() catch exceptions from inside the method

위 글과 같이 저도 똑같은 문제가 생겼는데 DispatchProxy의 Invoke는 Task를 반환하지 않기 때문에 Invoke를 catch로 Exception을 잡을 수 없습니다.

invoke에서 Exception이 생겨도 After를 실행 시켜버리기 때문에 Exception 전까지 데이터가 commit이 되어 버립니다.

Castle.DynamicProxy로 해결

이리저리 방법을 찾던 중 Castle 프로젝트에서 만든 DynamicProxy로 해결할 수 있었습니다.

public class MyInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        before();
        invocation.Proceed();
        after();
    }
}

위와 같이 기본적으로 제공해주는 Interceptor의 경우 Task를 반환하지 않기 때문에 다른 방법을 사용해야 합니다.

AsyncInterceptorBase

AsyncInterceptorBase를 사용하면 Task를 반환하여 Exception을 Catch 할 수 있습니다.

하지만 주의 사항으로 ProxyGenerator를 통해 등록하지 않으면 스레드 부족이나 교착상태가 발생할 수 있으므로 꼭 주의해서 사용해야합니다.

ProxyGenerator 등록


public class UserService : IUserService
{
    private IUserRepo userRepo;
    private ICircleRepo circleRepo;



services.AddTransient(sp =>
{
    var service = new UserService(sp.GetRequiredService<IUserRepo>()!, sp.GetRequiredService<ICircleRepo>()!);
    var generator = new ProxyGenerator();
    var interceptor = new Intercepter();
    var proxy = generator.CreateInterfaceProxyWithTargetInterface<IUserService>(service, interceptor);
    return proxy;
});

Interceptor

public class Intercepter : AsyncInterceptorBase
{
    private TransactionScope _scope { set; get; } = default!;
    private TransactionOptions _option { set; get; } = default!;
    public Intercepter(IsolationLevel isolationLevel = IsolationLevel.Serializable, long secondstimeout = 30)
    {
        _option = new TransactionOptions
        {
            IsolationLevel = isolationLevel,
            Timeout = new TimeSpan(secondstimeout * 10000000L)
        };
    }

    protected override async Task InterceptAsync(IInvocation invocation, IInvocationProceedInfo proceedInfo, Func<IInvocation, IInvocationProceedInfo, Task> proceed)
    {
        try
        {
            var targetMethod = invocation.MethodInvocationTarget;
            var hasAttribute = targetMethod!.GetCustomAttributes(true).Any(a => a.GetType().GetInterfaces().Any(k => k == typeof(IAspectAttibute)));
            if (!hasAttribute)
            {
                await proceed(invocation, proceedInfo).ConfigureAwait(false);
            }
            else
            {
                _scope = new TransactionScope(TransactionScopeOption.Required, _option);
                await proceed(invocation, proceedInfo).ConfigureAwait(false);
                _scope.Complete();
                _scope.Dispose();
            }
        }
        catch 
        {
            throw;
        }
    }

    protected override async Task<TResult> InterceptAsync<TResult>(IInvocation invocation, IInvocationProceedInfo proceedInfo, Func<IInvocation, IInvocationProceedInfo, Task<TResult>> proceed)
    {
        try
        {
            var targetMethod = invocation.MethodInvocationTarget;
            var hasAttribute = targetMethod!.GetCustomAttributes(true).Any(a => a.GetType().GetInterfaces().Any(k => k == typeof(IAspectAttibute)));
            if (!hasAttribute)
            {
                return await proceed(invocation, proceedInfo).ConfigureAwait(false);
            }
            else
            {
                _scope = new TransactionScope(TransactionScopeOption.Required, _option);
                var result = await proceed(invocation, proceedInfo).ConfigureAwait(false);
                _scope.Complete();
                _scope.Dispose();
                return result;
            }
        }
        catch
        {
            throw;
        }
    }
}

'Language > C#' 카테고리의 다른 글

Task vs. ValueTask  (0) 2023.06.17
dotnet cli nuget 저장소 지정  (0) 2023.06.16
StringBuilder vs String Join  (0) 2023.06.04
상속에서 Dispose 패턴  (0) 2023.05.31
EF Core Fluent API Entity Configuration  (0) 2023.05.17