소프트웨어 아키텍처에서 명령(Command) 패턴은 특정 작업을 캡슐화하여 요청을 하는 객체와 요청을 수행하는 객체(Receiver)를 분리하는 디자인 패턴입니다. 이는 곧 작업의 발행(issue)과 실행(execution) 시점을 분리하고, 요청을 취소하거나 기록하는 등의 유연성을 제공합니다. 명령 패턴은 이벤트 기반 시스템, 명령 핸들러(Command Handler) 구조, 그리고 관점 지향 프로그래밍(AOP)의 개념과도 밀접하게 연결될 수 있습니다. 특히 대량의 데이터 처리나 고성능이 요구되는 백엔드 시스템에서는 이러한 패턴의 효율적인 구현이 중요합니다.
이 글에서는 명령 패턴의 기본 개념을 넘어, .NET의 TPL Dataflow 라이브러리를 활용하여 고성능 비동기 명령 처리기(Command Processor 또는 Message Dispatcher)를 구현하는 방법을 심층적으로 탐구합니다. 이는 복잡한 백엔드 작업을 효율적으로 관리하고 처리량을 극대화하는 데 기여합니다.
아래는 TPL Dataflow를 기반으로 구축된 고성능 명령 처리기의 핵심 코드입니다.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace CommandProcessing.Dataflow
{
// 메시지 처리 시스템의 핵심 인터페이스 및 데이터 모델 정의
public interface ICommand<TResult> { }
public interface ICommandHandler<TCommand, TResult> where TCommand : ICommand<TResult>
{
Task<TResult> HandleAsync(TCommand command, CancellationToken cancellationToken);
}
public interface ICommandPipelineBehavior<TCommand, TResult> where TCommand : ICommand<TResult>
{
Task<TResult> Handle(TCommand command, Func<Task<TResult>> next, CancellationToken cancellationToken);
}
public interface IMessageDispatcher
{
Task<TResult> DispatchAsync<TCommand, TResult>(TCommand command, CancellationToken ct = default)
where TCommand : ICommand<TResult>;
}
// 처리기 모니터링을 위한 메트릭 클래스
public class ProcessorMetrics
{
public long CompletedOperations { get; set; }
public long FailedOperations { get; set; }
public TimeSpan TotalProcessingDuration { get; set; }
public TimeSpan AverageProcessingDuration { get; set; }
public int ActiveWorkers { get; set; }
public int MaxWorkerCapacity { get; set; }
public int InputQueueCount { get; set; }
}
/// <summary>
/// TPL Dataflow를 활용한 고성능 비동기 명령 처리기 구현
/// 병렬 처리, 백프레셔 제어, 처리 모니터링 기능 포함
/// </summary>
public class ConcurrentCommandProcessor : IMessageDispatcher, IDisposable
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<ConcurrentCommandProcessor>? _logService;
private readonly ConcurrentDictionary<Type, Func<object>> _handlerFactories = new();
private readonly ConcurrentDictionary<Type, Func<object[]>> _behaviorFactories = new();
// TPL Dataflow ActionBlock을 사용하여 비동기 명령을 처리
private ActionBlock<CommandExecutionRequest> _executionBlock = null!;
// 백프레셔 제어를 위한 세마포어
private readonly SemaphoreSlim _concurrencyGovernor;
private readonly int _maxParallelism;
// 성능 모니터링 지표
private long _completedCount;
private long _errorCount;
private long _totalExecutionTimeTicks;
public ConcurrentCommandProcessor(IServiceProvider serviceProvider, ILogger<ConcurrentCommandProcessor>? logger = null,
int? maxParallelism = null)
{
_serviceProvider = serviceProvider;
_logService = logger;
_maxParallelism = maxParallelism ?? Environment.ProcessorCount * 2;
_concurrencyGovernor = new SemaphoreSlim(_maxParallelism, _maxParallelism);
InitializeProcessingPipeline();
}
private void InitializeProcessingPipeline()
{
// ActionBlock 설정: 최대 병렬 처리 수준과 큐 용량 지정
_executionBlock = new ActionBlock<CommandExecutionRequest>(
async request =>
{
try
{
await _concurrencyGovernor.WaitAsync(); // 병렬 처리 제한
var operationStartTime = DateTime.UtcNow;
// 실제 명령 처리 파이프라인 실행
var executionResult = await ExecuteCommandPipelineInternal(request);
var duration = DateTime.UtcNow - operationStartTime;
Interlocked.Add(ref _totalExecutionTimeTicks, duration.Ticks);
Interlocked.Increment(ref _completedCount);
request.CompletionSource.SetResult(executionResult);
}
catch (Exception ex)
{
Interlocked.Increment(ref _errorCount);
_logService?.LogError(ex, "명령 처리 중 오류 발생: {CommandTypeName}", request.CommandType.Name);
request.CompletionSource.SetException(ex);
}
finally
{
_concurrencyGovernor.Release(); // 세마포어 해제
}
},
new ExecutionDataflowBlockOptions
{
MaxDegreeOfParallelism = _maxParallelism,
BoundedCapacity = _maxParallelism * 2 // 입력 큐 용량 제한
});
}
/// <summary>
/// 주어진 명령을 비동기적으로 디스패치하고 결과를 반환합니다.
/// </summary>
/// <typeparam name="TCommand">처리할 명령 타입</typeparam>
/// <typeparam name="TResult">명령 실행 결과 타입</typeparam>
/// <param name="command">실행할 명령 객체</param>
/// <param name="ct">취소 토큰</param>
/// <returns>명령 실행 결과</returns>
/// <exception cref="InvalidOperationException">명령 큐에 추가 실패 시</exception>
public async Task<TResult> DispatchAsync<TCommand, TResult>(TCommand command, CancellationToken ct = default)
where TCommand : ICommand<TResult>
{
var commandType = typeof(TCommand);
var uniqueId = Guid.NewGuid();
var taskCompletionSource = new TaskCompletionSource<object>();
var processingRequest = new CommandExecutionRequest(uniqueId, commandType, typeof(TResult), command, taskCompletionSource);
// TPL Dataflow ActionBlock에 명령 게시 (Post)
if (!_executionBlock.Post(processingRequest))
{
throw new InvalidOperationException("명령을 처리 대기열에 추가할 수 없습니다 - 시스템 과부하 가능성");
}
try
{
var finalResult = await taskCompletionSource.Task.WaitAsync(ct);
return (TResult)finalResult;
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
_logService?.LogWarning("명령 {CommandTypeName}이(가) 취소되었습니다", commandType.Name);
throw;
}
}
// 리플렉션을 사용하여 제네릭 파이프라인 메서드를 호출
private async Task<object> ExecuteCommandPipelineInternal(CommandExecutionRequest request)
{
var processMethod = typeof(ConcurrentCommandProcessor).GetMethod(nameof(ExecuteGenericPipeline), BindingFlags.NonPublic | BindingFlags.Instance);
var genericProcessorMethod = processMethod!.MakeGenericMethod(request.CommandType, request.ResultType);
var executionTask = (Task)genericProcessorMethod.Invoke(this, new object[] { request })!;
await executionTask;
var resultAccessor = executionTask.GetType().GetProperty("Result");
return resultAccessor?.GetValue(executionTask) ?? throw new InvalidOperationException("작업 결과 가져오기 실패");
}
// 실제 제네릭 파이프라인 처리 로직
private async Task<TResult> ExecuteGenericPipeline<TCommand, TResult>(CommandExecutionRequest request)
where TCommand : ICommand<TResult>
{
// 핸들러 및 비헤이비어 팩토리 캐시에서 가져오거나 생성
var handlerCreator = GetOrCreateHandlerFactory<TCommand, TResult>(request.CommandType);
var behaviorCreators = GetOrCreateBehaviorFactories<TCommand, TResult>(request.CommandType);
// 인스턴스 생성
var handlerInstance = handlerCreator();
var behaviorInstances = behaviorCreators();
// 핸들러 호출을 초기 파이프라인 단계로 설정
Func<Task<TResult>> currentPipelineStep = () => InvokeHandler<TCommand, TResult>(handlerInstance, (TCommand)request.CommandInstance);
// 비헤이비어를 역순으로 적용하여 파이프라인 구성 (안쪽부터 실행되도록)
foreach (var behavior in behaviorInstances.Reverse())
{
var specificBehavior = behavior;
var nextStep = currentPipelineStep; // 클로저를 위해 캡처
currentPipelineStep = async () => (TResult)await ApplyBehavior(specificBehavior, (TCommand)request.CommandInstance, nextStep);
}
return await currentPipelineStep(); // 파이프라인 실행
}
// 단일 비헤이비어 실행 로직
private async Task<object> ApplyBehavior<TCommand, TResult>(
ICommandPipelineBehavior<TCommand, TResult> behaviorInstance,
TCommand commandObject,
Func<Task<TResult>> nextOperation)
where TCommand : ICommand<TResult>
{
try
{
var behaviorResult = await behaviorInstance.Handle(commandObject, nextOperation, CancellationToken.None);
return behaviorResult!;
}
catch (Exception ex)
{
throw new InvalidOperationException($"행동 실행 중 오류 발생 {behaviorInstance.GetType().Name}: {ex.Message}", ex);
}
}
// 명령 핸들러 팩토리 캐시 로직
private Func<ICommandHandler<TCommand, TResult>> GetOrCreateHandlerFactory<TCommand, TResult>(Type commandType)
where TCommand : ICommand<TResult>
{
return (Func<ICommandHandler<TCommand, TResult>>)_handlerFactories.GetOrAdd(commandType, _ =>
{
return new Func<ICommandHandler<TCommand, TResult>>(() =>
{
using var scope = _serviceProvider.CreateScope();
var resolvedHandler = scope.ServiceProvider.GetService<ICommandHandler<TCommand, TResult>>();
if (resolvedHandler == null)
throw new InvalidOperationException($"{commandType.Name}에 대한 핸들러가 등록되지 않았습니다.");
return resolvedHandler;
});
});
}
// 파이프라인 비헤이비어 팩토리 캐시 로직
private Func<ICommandPipelineBehavior<TCommand, TResult>[]> GetOrCreateBehaviorFactories<TCommand, TResult>(Type commandType)
where TCommand : ICommand<TResult>
{
return (Func<ICommandPipelineBehavior<TCommand, TResult>[]>)_behaviorFactories.GetOrAdd(commandType, _ =>
{
return new Func<ICommandPipelineBehavior<TCommand, TResult>[]>(() =>
{
using var scope = _serviceProvider.CreateScope();
var resolvedBehaviors = scope.ServiceProvider.GetServices<ICommandPipelineBehavior<TCommand, TResult>>().ToArray();
return resolvedBehaviors;
});
});
}
// 핸들러의 HandleAsync 메서드 호출
private async Task<TResult> InvokeHandler<TCommand, TResult>(ICommandHandler<TCommand, TResult> handlerInstance, TCommand commandObject)
where TCommand : ICommand<TResult>
{
return await handlerInstance.HandleAsync(commandObject, CancellationToken.None);
}
// --- 리플렉션 기반 핸들러/비헤이비어 해소를 위한 비제네릭 팩토리 (내부 사용) ---
// 이 메서드들은 제네릭 메서드와 동일한 기능을 수행하지만, 'object'를 사용하여 리플렉션 호출을 용이하게 합니다.
private Func<object> GetOrCreateUntypedHandlerFactory(Type commandType)
{
return _handlerFactories.GetOrAdd(commandType, _ =>
{
var commandInterface = commandType.GetInterfaces()
.FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICommand<>));
if (commandInterface == null)
throw new InvalidOperationException($"명령 타입 {commandType.Name}은(는) ICommand<TResult>를 구현하지 않습니다.");
var resultType = commandInterface.GetGenericArguments()[0];
var handlerInterfaceType = typeof(ICommandHandler<,>).MakeGenericType(commandType, resultType);
return new Func<object>(() =>
{
using var scope = _serviceProvider.CreateScope();
var resolvedHandler = scope.ServiceProvider.GetService(handlerInterfaceType);
if (resolvedHandler == null)
throw new InvalidOperationException($"{commandType.Name}에 대한 핸들러가 등록되지 않았습니다.");
return resolvedHandler;
});
});
}
private Func<object[]> GetOrCreateUntypedBehaviorFactories(Type commandType)
{
return _behaviorFactories.GetOrAdd(commandType, _ =>
{
var commandInterface = commandType.GetInterfaces()
.FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICommand<>));
if (commandInterface == null)
throw new InvalidOperationException($"명령 타입 {commandType.Name}은(는) ICommand<TResult>를 구현하지 않습니다.");
var resultType = commandInterface.GetGenericArguments()[0];
var behaviorInterfaceType = typeof(ICommandPipelineBehavior<,>).MakeGenericType(commandType, resultType);
return new Func<object[]>(() =>
{
using var scope = _serviceProvider.CreateScope();
var resolvedBehaviors = scope.ServiceProvider.GetServices(behaviorInterfaceType).Where(b => b != null).ToArray();
return resolvedBehaviors!;
});
});
}
/// <summary>
/// 현재 처리기의 성능 메트릭을 반환합니다.
/// </summary>
public ProcessorMetrics GetOperationMetrics()
{
return new ProcessorMetrics
{
CompletedOperations = Interlocked.Read(ref _completedCount),
FailedOperations = Interlocked.Read(ref _errorCount),
TotalProcessingDuration = TimeSpan.FromTicks(Interlocked.Read(ref _totalExecutionTimeTicks)),
AverageProcessingDuration = _completedCount > 0
? TimeSpan.FromTicks(Interlocked.Read(ref _totalExecutionTimeTicks) / _completedCount)
: TimeSpan.Zero,
ActiveWorkers = _concurrencyGovernor.CurrentCount,
MaxWorkerCapacity = _maxParallelism,
InputQueueCount = _executionBlock.InputCount
};
}
/// <summary>
/// 캐시된 핸들러 및 비헤이비어 팩토리를 지웁니다.
/// </summary>
public void ResetCaches()
{
_handlerFactories.Clear();
_behaviorFactories.Clear();
}
/// <summary>
/// 리소스 해제
/// </summary>
public void Dispose()
{
_executionBlock?.Complete();
_concurrencyGovernor?.Dispose();
}
}
// TPL Dataflow ActionBlock으로 전달될 명령 요청을 캡슐화하는 내부 클래스
internal class CommandExecutionRequest
{
public Guid OperationId { get; }
public Type CommandType { get; }
public Type ResultType { get; }
public object CommandInstance { get; }
public TaskCompletionSource<object> CompletionSource { get; }
public CommandExecutionRequest(Guid id, Type commandType, Type resultType, object command, TaskCompletionSource<object> tcs)
{
OperationId = id;
CommandType = commandType;
ResultType = resultType;
CommandInstance = command;
CompletionSource = tcs;
}
}
}
위의 TPL Dataflow 기반 처리기는 고성능 환경에 최적화되어 있지만, 모든 상황에 필요한 것은 아닙니다. 비교적 간단하거나 동기적인 처리가 요구되는 시나리오에서는 표준적인 명령 처리기 구현이 더 적합할 수 있습니다. 아래는 TPL Dataflow 없이 파이프라인 패턴을 적용한 일반적인 메시지 디스패처 구현입니다.
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
namespace CommandProcessing.Sequential
{
// 메시지 처리 시스템의 핵심 인터페이스 및 데이터 모델 정의 (Dataflow 예제와 동일)
public interface ICommand<TResult> { }
public interface ICommandHandler<TCommand, TResult> where TCommand : ICommand<TResult>
{
Task<TResult> HandleAsync(TCommand command, CancellationToken cancellationToken);
}
public interface ICommandPipelineBehavior<TCommand, TResult> where TCommand : ICommand<TResult>
{
Task<TResult> Handle(TCommand command, Func<Task<TResult>> next, CancellationToken cancellationToken);
}
public interface IMessageDispatcher
{
Task<TResult> DispatchAsync<TCommand, TResult>(TCommand command, CancellationToken ct = default)
where TCommand : ICommand<TResult>;
}
/// <summary>
/// 표준 동기/비동기 명령 처리를 위한 메시지 디스패처
/// 파이프라인 행동(Behavior)을 적용하여 횡단 관심사를 분리
/// </summary>
public class SynchronousMessageDispatcher : IMessageDispatcher
{
private readonly IServiceProvider _serviceProvider;
private readonly ConcurrentDictionary<Type, Func<object>> _handlerResolutionCache = new();
private readonly ConcurrentDictionary<Type, Func<object[]>> _behaviorResolutionCache = new();
private readonly ConcurrentDictionary<Type, Func<object, object, CancellationToken, Task<object>>> _commandPipelineCache = new();
public SynchronousMessageDispatcher(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
/// <summary>
/// 캐시된 모든 핸들러, 비헤이비어, 파이프라인 정의를 지웁니다.
/// </summary>
public void ClearAllCaches()
{
_handlerResolutionCache.Clear();
_behaviorResolutionCache.Clear();
_commandPipelineCache.Clear();
}
/// <summary>
/// 주어진 명령을 비동기적으로 디스패치하고 결과를 반환합니다.
/// </summary>
/// <typeparam name="TCommand">처리할 명령 타입</typeparam>
/// <typeparam name="TResult">명령 실행 결과 타입</typeparam>
/// <param name="command">실행할 명령 객체</param>
/// <param name="ct">취소 토큰</param>
/// <returns>명령 실행 결과</returns>
public async Task<TResult> DispatchAsync<TCommand, TResult>(TCommand command, CancellationToken ct = default)
where TCommand : ICommand<TResult>
{
var commandType = typeof(TCommand);
// 핸들러 인스턴스 해소
var handlerInstance = ResolveHandler<TCommand, TResult>(commandType);
// 명령 파이프라인 캐시에서 가져오거나 생성
var commandPipeline = GetOrCreateProcessingPipeline<TCommand, TResult>(commandType);
// 파이프라인 실행
var finalResult = await commandPipeline(handlerInstance, command, ct);
return (TResult)finalResult;
}
// 특정 명령에 대한 핸들러를 DI 컨테이너에서 해소하고 캐시
private ICommandHandler<TCommand, TResult> ResolveHandler<TCommand, TResult>(Type commandType)
where TCommand : ICommand<TResult>
{
var handlerFactory = (Func<object>)_handlerResolutionCache.GetOrAdd(commandType, _ =>
{
return new Func<object>(() =>
{
using var scope = _serviceProvider.CreateScope();
var handler = scope.ServiceProvider.GetService(typeof(ICommandHandler<TCommand, TResult>));
if (handler == null)
throw new InvalidOperationException($"{commandType.Name}에 대한 핸들러가 등록되지 않았습니다.");
return handler;
});
});
return (ICommandHandler<TCommand, TResult>)handlerFactory();
}
// 특정 명령에 대한 비헤이비어 목록을 DI 컨테이너에서 해소하고 캐시
private ICommandPipelineBehavior<TCommand, TResult>[] ResolveBehaviors<TCommand, TResult>(Type commandType)
where TCommand : ICommand<TResult>
{
var behaviorsFactory = (Func<object[]>)_behaviorResolutionCache.GetOrAdd(commandType, _ =>
{
return new Func<object[]>(() =>
{
using var scope = _serviceProvider.CreateScope();
var behaviors = scope.ServiceProvider.GetServices<ICommandPipelineBehavior<TCommand, TResult>>().ToArray();
return behaviors.Cast<object>().ToArray();
});
});
return behaviorsFactory().Cast<ICommandPipelineBehavior<TCommand, TResult>>().ToArray();
}
// 명령 파이프라인을 구축하거나 캐시에서 가져옵니다.
private Func<object, object, CancellationToken, Task<object>> GetOrCreateProcessingPipeline<TCommand, TResult>(Type commandType)
where TCommand : ICommand<TResult>
{
return _commandPipelineCache.GetOrAdd(commandType, _ =>
{
var orderedBehaviors = ResolveBehaviors<TCommand, TResult>(commandType);
// 파이프라인을 미리 구축하여 매 호출 시 재구축 비용 절감
return async (handlerObject, commandObject, cancellationToken) =>
{
if (handlerObject == null || commandObject == null)
throw new ArgumentNullException("핸들러 또는 명령은 null일 수 없습니다.");
var typedHandler = (ICommandHandler<TCommand, TResult>)handlerObject;
var typedCommand = (TCommand)commandObject;
// 비헤이비어가 없으면 핸들러를 직접 호출
if (orderedBehaviors.Length == 0)
{
var result = await typedHandler.HandleAsync(typedCommand, cancellationToken);
return (object)result!;
}
// 재귀 방식으로 비헤이비어 체인 실행
var pipelineResult = await ExecuteBehaviorChain(typedHandler, typedCommand, orderedBehaviors, 0, cancellationToken);
return (object)pipelineResult!;
};
});
}
// 파이프라인 내의 비헤이비어 체인을 실행하는 재귀 메서드
private async Task<TResult> ExecuteBehaviorChain<TCommand, TResult>(
ICommandHandler<TCommand, TResult> currentHandler,
TCommand currentCommand,
ICommandPipelineBehavior<TCommand, TResult>[] allBehaviors,
int behaviorPosition,
CancellationToken cancellationToken)
where TCommand : ICommand<TResult>
{
// 모든 비헤이비어를 처리했으면 최종 핸들러 실행
if (behaviorPosition >= allBehaviors.Length)
{
return await currentHandler.HandleAsync(currentCommand, cancellationToken);
}
var nextBehavior = allBehaviors[behaviorPosition];
return await nextBehavior.Handle(currentCommand, () => ExecuteBehaviorChain(currentHandler, currentCommand, allBehaviors, behaviorPosition + 1, cancellationToken), cancellationToken);
}
}
}
이러한 명령 처리기는 파이프라인에 ICommandPipelineBehavior와 같은 확장 지점을 제공하여, 비즈니스 로직 전후에 특정 작업을 삽입할 수 있습니다. 예를 들어, 로깅, 유효성 검사, 트랜잭션 관리 등을 여기에 적용할 수 있습니다. 이 방식은 관점 지향 프로그래밍(AOP)과 유사한 목적을 가지며, 횡단 관심사(cross-cutting concerns)를 비즈니스 로직에서 분리하는 데 기여합니다. 그렇다면 명령 처리기의 파이프라인 모델과 AOP는 어떤 공통점과 차이점을 가질까요?
공통점
- 목표 일치: 둘 다 로깅, 유효성 검사, 트랜잭션, 캐싱 등과 같은 횡단 관심사를 핵심 비즈니스 로직에서 분리하는 것을 목표로 합니다.
- 호출 체인 패턴: AOP의 인터셉터 체인이나 명령 버스의 비헤이비어 파이프라인은 모두 여러 계층을 거쳐 최종적으로 실제 비즈니스 로직을 실행하는 구조를 가집니다.
- 유연한 확장성: 비즈니스 코드를 변경하지 않고도 특정 횡단 로직을 동적으로 추가하거나 제거할 수 있습니다.
차이점
| 특성 | AOP (동적 프록시 / 인터셉터) | 명령 버스 + 비헤이비어 |
|---|---|---|
| 트리거 방식 | 메서드 호출 시점 자동 가로채기 (프록시/동적 프록시 기반) | 명령 버스를 통한 명령 실행 시 파이프라인 명시적 통과 |
| 적용 범위 | 광범위 (클래스의 거의 모든 메서드에 적용 가능) | 명령(Command) 처리로 제한 (CQRS 맥락에 특화) |
| 기술 구현 | DI 컨테이너 인터셉션, 미들웨어 또는 컴파일 타임 주입에 의존 | 파이프라인 패턴에 의존 (예: MediatR의 IPipelineBehavior) |
| 유연성 | 더욱 범용적이며 프로젝트 전체에 걸쳐 적용 가능 (예: 서비스 계층 모든 메서드에 로깅 추가) | 명령/쿼리 실행 체인에 집중, 특정 작업에 대한 타겟팅이 강력 |
| 침투성 | 낮음, 비즈니스 코드를 거의 변경하지 않음 (인터페이스/가상 메서드 필요) | 명령 버스를 통해 명령을 보내야 하므로 특정 코드 구조를 따름 |
성능 비교 요약
| 측면 | AOP (동적 프록시) | 명령 버스 + 비헤이비어 |
|---|---|---|
| 호출 오버헤드 | 동적 프록시/리플렉션 사용으로 상대적으로 높음 | 일반적인 메서드 호출로 상대적으로 낮음 |
| 확장성 | 전역적으로 범용적 | 국소적 (명령/쿼리 처리) |
| 성능 손실 | 상대적으로 높음 (특히 고빈도 호출 경로에서) | 상대적으로 낮음 |
| 적합한 시나리오 | 횡단 관심사, 범용적인 기능 (예: 애플리케이션 전체 로깅) | CQRS 비즈니스 파이프라인, 고성능이 요구되는 명령 처리 |
두 방식을 한 문장으로 요약하자면:
- AOP는 더 범용적이지만 성능 오버헤드가 있을 수 있습니다.
- 명령 버스 + 비헤이비어는 더 효율적이지만 적용 범위가 제한적입니다.