필터란 무엇인가?
ASP.NET Core에서 필터(Filter)는 컨트롤러의 액션 메서드 실행 전후에 특정 로직을 삽입할 수 있는 메커니즘입니다. 이를 통해 인증, 예외 처리, 로깅, 캐싱 등과 같은 공통 관심사(cross-cutting concerns)를 중복 코드 없이 일관되게 적용할 수 있습니다.
필터는 MVC 요청 처리 파이프라인 내에서 정해진 순서로 실행됩니다. 이 파이프라인은 라우팅 후 실행할 액션이 결정된 이후 시작되며, 다섯 가지 유형의 필터가 단계별로 작동합니다:
- Authorization Filter: 요청 초기 단계에서 접근 권한을 검사합니다.
- Resource Filter: 모델 바인딩 전후로 리소스 수준 제어를 수행합니다.
- Action Filter: 액션 메서드 호출 전후에 실행됩니다.
- Exception Filter: 처리되지 않은 예외를 캐치하고 사용자 정의 응답을 제공합니다.
- Result Filter: 결과(Action Result) 실행 전후에 개입합니다.
모든 필터는 IFilterMetadata 인터페이스를 기반으로 하며, 동기 및 비동기 방식 모두를 지원합니다.
비동기 vs 동기 필터 구현
동기 필터는 각 단계별로 Before와 After 두 개의 메서드를 정의합니다. 반면 비동기 필터는 단일 메서드에서 await next() 호출을 기준으로 전후 로직을 분리합니다.
다음은 비동기 액션 필터의 예시입니다:
public class LoggingActionFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
Console.WriteLine("액션 시작 전: 로그 기록");
var resultContext = await next();
if (resultContext.Exception == null)
{
Console.WriteLine("액션 성공적으로 완료");
}
else
{
Console.WriteLine("액션 실행 중 예외 발생");
resultContext.ExceptionHandled = true;
}
}
}
예외 필터로 글로벌 오류 처리
IExceptionFilter 또는 IAsyncExceptionFilter를 구현하여 애플리케이션 전역에서 예외를 중앙 집중식으로 처리할 수 있습니다. 특히 개발 환경에서는 예외 정보를 클라이언트에게 전달하는 것이 디버깅에 유용합니다.
다음은 개발 환경에서만 예외 스택 트레이스를 응답 본문에 포함하는 예외 필터입니다:
public class GlobalExceptionFilter : IExceptionFilter
{
private readonly IHostEnvironment _env;
public GlobalExceptionFilter(IHostEnvironment env) => _env = env;
public void OnException(ExceptionContext context)
{
if (!_env.IsDevelopment()) return;
context.Result = new ContentResult
{
StatusCode = 500,
Content = context.Exception.ToString()
};
context.ExceptionHandled = true;
}
}
이 필터를 전역으로 등록하려면 다음처럼 구성합니다:
builder.Services.Configure<MvcOptions>(options =>
{
options.Filters.Add<GlobalExceptionFilter>();
});
주의할 점은 예외 필터보다 미들웨어가 더 일반적이고 유연한 오류 처리 수단이라는 것입니다. 예외 필터는 특정 액션에 특화된 에러 응답이 필요할 때 주로 사용됩니다.
액션 필터로 실행 흐름 제어
액션 필터는 액션 메서드 실행 전후에 개입하여 매개변수 조작, 실행 차단, 결과 수정 등을 수행할 수 있습니다. 비동기 방식을 사용하면 보다 효율적인 비동기 작업 통합이 가능합니다.
여러 액션 필터를 동시에 사용할 경우, 등록된 순서대로 "감싸는" 형태로 실행됩니다. 즉, 먼저 등록된 필터가 가장 바깥쪽 래퍼처럼 동작합니다.
다음은 두 개의 사용자 정의 필터를 등록하고 액션에 적용하는 예제입니다:
builder.Services.Configure<MvcOptions>(options =>
{
options.Filters.Add<OuterActionFilter>();
options.Filters.Add<InnerActionFilter>();
});
컨트롤러에서는 속성(Attribute) 기반으로도 필터를 적용할 수 있습니다:
[ApiController]
[Route("[controller]")]
[TypeFilter(typeof(OuterActionFilter))]
[TypeFilter(typeof(InnerActionFilter))]
public class TestController : ControllerBase
{
[HttpGet]
public IActionResult GetData()
{
Console.WriteLine("GetData 메서드 실행");
return Ok("data");
}
}
출력 예시는 다음과 같습니다:
OuterActionFilter: 시작 InnerActionFilter: 시작 GetData 메서드 실행 InnerActionFilter: 완료 OuterActionFilter: 완료
이처럼 필터는 실행 시점에 따라 중첩된 구조로 동작하며, next() 호출 여부에 따라 후속 단계를 건너뛸 수도 있습니다.