모든 ASP.NET Core Web API 프로젝트에 필요한 것 - 3부 - 예외 처리 미들웨어

previous article에서 API 버전 관리 및 API 버전 관리를 지원하는 Swagger를 sample project에 추가하는 방법에 대해 썼습니다. 이 기사에서는 사용자 지정 미들웨어를 추가하여 전역적으로 예외를 처리하고 오류 발생 시 사용자 지정 응답을 생성하는 방법을 보여줍니다.

누가 버그 없는 코드를 작성할 수 있습니까? 적어도 나는 아니다. 각 시스템에서 처리되지 않은 예외가 발생할 수 있지만 오류를 잡아서 기록하고 수정하고 클라이언트에 적절한 응답을 표시하는 것이 정말 중요합니다. 예외 처리 미들웨어는 단일 위치에서 예외를 포착하고 응용 프로그램을 통해 중복된 예외 처리 코드를 방지하는 데 도움이 됩니다.

1단계 - 예외 처리 미들웨어 구현



먼저 Infrastructure 폴더에 새 폴더를 추가하고 Middlewares를 호출한 다음 새 파일ApiExceptionHandlingMiddleware.cs을 추가합니다. 다음 코드를 추가합니다.

public class ApiExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ApiExceptionHandlingMiddleware> _logger;

    public ApiExceptionHandlingMiddleware(RequestDelegate next, ILogger<ApiExceptionHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        _logger.LogError(ex, $"An unhandled exception has occurred, {ex.Message}");

        var problemDetails = new ProblemDetails
        {
            Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1",
            Title = "Internal Server Error",
            Status = (int)HttpStatusCode.InternalServerError,
            Instance = context.Request.Path,
            Detail = "Internal server error occured!"
        };

        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        var result = JsonSerializer.Serialize(problemDetails);

        context.Response.ContentType = "application/json";
        await context.Response.WriteAsync(result);
    }
}


응답의 상태 코드를 500context.Response.StatusCode = (int)HttpStatusCode.InternalServerError으로 설정하는 것과 함께 ProblemDetails 형식의 메시지가 응답 본문에 존재합니다.

ASP.NET Core 2.2 이전의 HTTP 400(BadRequest(ModelState)에 대한 기본 응답 유형은 다음과 같습니다.

{
  "": [
    "A non-empty request body is required."
  ]
}


IETF(Internet Engineering Task Force)RFC-7231 document에 따르면 ASP.NET Core 팀은 웹 API 응답의 오류를 지정하기 위해 기계가 읽을 수 있는 형식ProblemDetails을 구현했으며 RFC 7807 사양을 준수합니다.

2단계 - 미들웨어 등록


  • 미들웨어를 등록하기 위한 확장 메서드를 만듭니다.

  • public static class MiddlewareExtensions
    {
        public static IApplicationBuilder UseApiExceptionHandling(this IApplicationBuilder app)
            => app.UseMiddleware<ApiExceptionHandlingMiddleware>();
    }
    


  • Startup.cs 클래스를 열고 Configure 메서드에서 미들웨어를 추가합니다.

  • public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            ...
        }
    
        app.UseApiExceptionHandling();
    


    아시다시피 미들웨어 구성 요소를 추가하는 순서가 중요합니다. 개발 환경에 UseDeveloperExceptionPage를 사용하는 경우 그 뒤에 ApiExceptionHandling 미들웨어를 추가하십시오.

    3단계 - 비즈니스 오류를 도메인 예외로 변환



    예외를 throw해야 하는 경우에 대해 많은 논쟁이 있지만 예외를 throw해야 하는 경우는 다음과 같습니다.
  • 가장 중요한 이유는 프로세스를 완료하고 결과를 제공하는 것이 불가능하기 때문입니다(빠른 실패).

  • private async Task AddProductToBasketAsync(Guid productId)
    {
        var product = await _repository.GetProductByIdAsync(productId);
        if(product == null)
            throw new DomainException($"Product with id {productId} could not be found."); 
            // Or simply return null?
            // Or return an error code or warping response into another object that has `Succeeded` property like `IdentityResult`?
            // Or return a tuple (false, "Product with id {productId} could not be found.")?
    



  • 예외 발생의 단점 중 하나는 예외에 성능 비용이 든다는 것입니다. 고성능 애플리케이션을 작성하는 경우 예외를 발생시키면 성능이 저하될 수 있습니다.


    다시 말하지만, 예외는 정말로 예외적이어야 합니다.


  • 오류 코드 또는 래퍼 개체를 사용할 때의 단점은 무엇입니까?
  • 호출자 코드가 결과에서 오류 코드 또는 null을 확인하는 것을 잊고 실행을 계속할 수 있기 때문에 예외를 잘 던지는 것이 더 안전합니다. 메서드 호출 결과를 아래에서 위로 확인해야 합니다
  • .
  • 결과를 IdentityResult 과 같은 다른 개체로 래핑하는 경우 추가 힙 할당을 지불해야 합니다. 각 호출에 대해 성공적인 작업을 위해서도 추가 개체를 초기화해야 합니다. 서로 다른 입력으로 API를 100번 호출하면 몇 번이나 예외가 발생할 수 있습니까? 따라서 추가 객체 초기화(및 힙 할당)와 함께 예외를 발생시키는 비율이 동일하지 않습니다
  • .


    4단계 - DomainException 클래스 추가


  • 프로젝트 루트에 새 폴더를 만들고 이름을 지정Domain한 다음 다른 폴더를 추가합니다Exception.
  • 새 파일DomainException.csException 폴더에 추가:

  • public class DomainException : Exception
    {
        public DomainException(string message)
            : base(message)
        {
        }
    }
    


  • 예외 처리 미들웨어에서 잘못된 요청 결과DomainException를 포착하고 변환합니다.

  • private async Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        string result;
    
        **if (ex is DomainException)
        {
            var problemDetails = new ValidationProblemDetails(new Dictionary<string, string[]> { { "Error", new[] { ex.Message } } })
            {
                Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
                Title = "One or more validation errors occurred.",
                Status = (int)HttpStatusCode.BadRequest,
                Instance = context.Request.Path,
            };
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
            result = JsonSerializer.Serialize(problemDetails);
        }**
        else
        {
            _logger.LogError(ex, $"An unhandled exception has occurred, {ex.Message}");
            var problemDetails = new ProblemDetails
            {
                Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1",
                Title = "Internal Server Error.",
                Status = (int)HttpStatusCode.InternalServerError,
                Instance = context.Request.Path,
                Detail = "Internal Server Error!"
            };
            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
            result = JsonSerializer.Serialize(problemDetails);
        }
    
        context.Response.ContentType = "application/json";
        await context.Response.WriteAsync(result);
    }
    


    처리되지 않은 예외와 마찬가지로 DomainExceptionValidationProblemDetails로 번역했습니다. 나중에 DomainException를 사용하겠습니다. 작동 중인 도메인 예외를 테스트해 보겠습니다.

    [HttpGet("throw-domain-exception")]
    public IActionResult ThrowDomainError()
    {
        throw new DomainException("Product could not be found");
    }
    




    Github에서 이 연습의 소스 코드를 찾을 수 있습니다.

    좋은 웹페이지 즐겨찾기