.NET API의 사용자 정의 예외 처리

· 4분 읽기

예외는 좋지 않습니다. 우리도 알고 있죠? 하지만 우리가 그것들을 처리해야 한다면 어떨까요?

예를 들어 API에서 예외가 발생하면 사용자가 받는 응답에서 제거하고 싶은 많은 정보가 포함된 스택 메시지가 표시됩니다.

데모를 위해 dotnet API를 만들고 예외를 발생시키는 메서드를 추가했습니다.

[HttpGet]
[Route("GetWithoutExceptionHandler")]
public Task GetWithoutExceptionHandler()
{
    throw new Exception("This is a custom exception!");
}

예외를 던지면 다음과 같습니다.

System.Exception: This is a custom exception!
   at CustomExceptionHandleDemo.Controllers.WeatherForecastController.GetWithoutExceptionHandler()
   at lambda_method16(Closure , Object , Object[] )
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

이것은 정상으로 보입니다. 예외에서 얻은 정보이지만 저에게는 많은 정보가 표시됩니다. 라이브러리 등이 있는 경우 클라이언트, 프로젝트 또는 기타 항목에 관한 합리적인 정보를 보려고 하는 사람에게 표시될 수 있습니다.

Swagger에서는 다음과 같이 표시됩니다.

사용자 정의 예외 만들기

어떤 예외(이 경우에는 [[TOK_3]]])가 발생할지 알고 있으므로 이 단계를 피할 수 있지만, 나에게는 사용자 정의 예외를 갖는 것이 더 좋습니다. 왜냐하면 여러분이 던지는 것을 더 잘 제어할 수 있기 때문입니다.

이 경우에는 Exception에서 상속받은 CustomException 객체를 생성했습니다.

namespace CustomExceptionHandleDemo.Exceptions
{
    public class CustomException : Exception
    {
        /// <summary>
        /// Constructor for <see cref="CustomException"/>
        /// </summary>
        public CustomException() { }

        /// <summary>
        /// Constructor for <see cref="CustomException"/>
        /// </summary>
        /// <param cref="string" name="message">Parameter for message</param>
        public CustomException(string message) : base(message) { }

        /// <summary>
        /// Constructor for <see cref="CustomException"/>
        /// </summary>
        /// <param cref="string" name="message">Parameter for message</param>
        /// <param cref="Exception" name="inner">Parameter for inner</param>
        public CustomException(string message, Exception inner) : base(message, inner) { }
    }
}

사용자 정의 예외를 생성한 후 Exception 대신 CustomException을 던지도록 메서드를 업데이트해 보겠습니다.

[HttpGet]
[Route("GetWithoutExceptionHandler")]
public Task GetWithoutExceptionHandler()
{
    throw new CustomException("This is a custom exception!");
}

이것은 지금은 아무것도 변경하지 않지만 스택 추적은 던져진 객체가 Exception 대신 CustomException임을 보여줍니다. 스택 추적의 시작 부분을 살펴보십시오.

CustomExceptionHandleDemo.Exceptions.CustomException: This is a custom exception!
   at CustomExceptionHandleDemo.Controllers.WeatherForecastController.GetWithoutExceptionHandler()
   at lambda_method24(Closure , Object , Object[] )
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

ExceptionFilterAttribute 만들기

Microsoft는 예외가 발생한 후 예외를 처리하는 방법을 제공했습니다. 자세한 내용은 여기에서 확인할 수 있습니다.

그러나 지금까지 그들이 우리에게 제공한 문서는 다음과 같습니다:

작업에서 예외가 발생한 후 비동기적으로 실행되는 추상 필터입니다. 서브클래스는 OnException(ExceptionContext) 또는 OnExceptionAsync(ExceptionContext)를 재정의해야 하지만 둘 다 재정의할 수는 없습니다.

그럼 하나를 만들어 보겠습니다. OnException를 재정의할 CustomExceptionFilterAttribute를 생성하겠습니다.

using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc;
using CustomExceptionHandleDemo.Exceptions;

namespace CustomExceptionHandleDemo.ExceptionFilterAttributes
{
    public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
    {
        /// <summary>
        /// OnException
        /// </summary>
        /// <param cref="ExceptionContext" name="context">Parameter for context</param>
        public override void OnException(ExceptionContext context)
        {
            if (context.Exception is CustomException)
            {
                context.HttpContext.Response.StatusCode = 500;
                context.Result = new ObjectResult(context.Exception.Message);
            }
        }
    }
}

보시다시피, 우리는 ExceptionContext를 살펴보고 있는데, 예외가 CustomException 유형일 때 뭔가를 합니다.

이것은 우리가 반환할 응답과 상태 코드를 업데이트하는 것입니다.

상태 코드를 업데이트하려면 context.HttpContext.Response.StatusCode를 업데이트해야 하며, 결과를 반환하려면 ActionResult에서 상속된 객체를 제공하여 context.Result을 업데이트해야 합니다.

이것은 필터이므로 [CustomExceptionFilter]를 추가하여 무언가를 추가해야 함을 의미합니다.

필터 사용하기

이제 우리가 가지고 있는 메소드를 복제하고 이 필터를 추가하여 조치를 취하도록 하겠습니다. API 엔드포인트는 다음과 같습니다.

using CustomExceptionHandleDemo.ExceptionFilterAttributes;
using CustomExceptionHandleDemo.Exceptions;
using Microsoft.AspNetCore.Mvc;

namespace CustomExceptionHandleDemo.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        public WeatherForecastController()
        {
        }

        [HttpGet]
        [Route("GetWithoutExceptionHandler")]
        public Task GetWithoutExceptionHandler()
        {
            throw new CustomException("This is a custom exception!");
        }

        [HttpGet]
        [Route("GetWithExceptionHandler")]
        [CustomExceptionFilter]
        public Task GetWithExceptionHandler()
        {
            throw new CustomException("This is a custom exception!");
        }
    }
}

보시다시피 GetWithExceptionHandler라는 새로운 메서드가 있습니다. 이는 GetWithoutExceptionHandler와 동일한 논리를 가지고 있지만 이 경우에는 메서드에 [CustomExceptionFilter] 필터를 추가했습니다.

메서드를 실행한 후 결과는 다음과 같습니다. 더 이상 스택 추적이 표시되지 않으므로 이미지를 표시하겠습니다.

따라서 이를 통해 우리는 사용자 정의 예외, 즉 예외를 던지고 메서드에서 사용할 때 발생하는 일을 재정의하는 필터를 만들었습니다.

이는 로깅 및 오류가 언제, 어디서 발생하는지 파악하는 등 많은 작업에 사용될 수 있습니다.