[Spring] API 예외 처리 - ExceptionResolver

JJoSuk·2023년 6월 12일
0

본 프로젝트 자료는 김영한님의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술을 참고 제작됐음을 알립니다.

API 예외 처리 - 스프링이 제공하는 ExceptionResolver1

스프링 부트가 기본으로 제공하는 ExceptionResolver 는 다음과 같다.

HandlerExceptionResolverComposite 에 다음 순서로 등록

1. ExceptionHandlerExceptionResolver

  • @ExceptionHandler 을 처리한다. API 예외 처리는 대부분 이 기능으로 해결한다. 조금 뒤에 자세히 설명한다.

2. ResponseStatusExceptionResolver

  • HTTP 상태 코드를 지정해준다.
    예) @ResponseStatus(value = HttpStatus.NOT_FOUND)

3. DefaultHandlerExceptionResolver 우선 순위가 가장 낮다.

  • 스프링 내부 기본 예외를 처리한다.

먼저 가장 쉬운 ResponseStatusExceptionResolver 부터 알아보자.

1. ResponseStatusExceptionResolver

ResponseStatusExceptionResolver 는 예외에 따라서 HTTP 상태 코드를 지정해주는 역할을 한다.

다음 2가지 겨우를 처리하는데,

  • @ResponseStatus 가 달려있는 예외
  • ResponseStatusException 예외

우선 @ResponseStatus

BadRequestException - 신규 클래스 생성

예외에 다음과 같이 @ResponseStatus 애노테이션을 적용하면 HTTP 상태 코드를 변경해준다.

BadRequestException 예외가 컨트롤러 밖으로 넘어가면 ResponseStatusExceptionResolver 예외가 해당 애노테이션을 확인해서 오류 코드를 HttpStatus.BAD_REQUEST (400)으로 변경하고, 메시지도 담는다.

  • ResponseStatusExceptionResolver 코드를 확인해보면 결국 response.sendError(statusCode, resolvedReason) 를 호출하는 것을 확인할 수 있다.
  • sendError(400) 를 호출했기 때문에 WAS에서 다시 오류 페이지( /error )를 내부 요청한다.

ApiExceptionController - 추가

실행 결과

{
        "status": 400,
        "error": "Bad Request",
		"exception": "hello.exception.exception.BadRequestException", 
        "message": "잘못된 요청 오류",
		"path": "/api/response-status-ex1"

}

메시지 기능

BadRequestException - 수정

reason 을 MessageSource 에서 찾는 기능도 제공한다. reason = "error.bad"

messages.properties - 프로퍼티스 신규 생성

PostMan 출력 결과

{
        "status": 400,
		"error": "Bad Request",
		"exception": "hello.exception.exception.BadRequestException", 
        "message": "잘못된 요청 오류입니다. 메시지 사용",
		"path": "/api/response-status-ex1"

}

ResponseStatusException

@ResponseStatus 는 개발자가 직접 변경할 수 없는 예외에는 적용할 수 없다. (애노테이션을 직접 넣어야 하는데, 내가 코드를 수정할 수 없는 라이브러리의 예외 코드 같은 곳에는 적용할 수 없다.)

추가로 애노테이션을 사용하기 때문에 조건에 따라 동적으로 변경하는 것도 어렵다. 이때는 ResponseStatusException 예외를 사용하면 된다.

ApiExceptionController - 추가

PostMan 출력 결과

{
        "status": 400,
		"error": "Bad Request",
		"exception": "hello.exception.exception.BadRequestException", 
        "message": "잘못된 요청 오류입니다. 메시지 사용",
		"path": "/api/response-status-ex1"
}

API 예외 처리 - 스프링이 제공하는 ExceptionResolver2

DefaultHandlerExceptionResolver 를 알아보자.

  • DefaultHandlerExceptionResolver 는 스프링 내부에서 발생하는 스프링 예외를 해결한다.
  • 대표적으로 파라미터 바인딩 시점에 타입이 맞지 않으면 내부에서 TypeMismatchException 이 발생하는데, 이 경우 예외가 발생했기 때문에 그냥 두면 서블릿 컨테이너까지 오류가 올라가고, 결과적으로 500 오류가 발생한다.
  • 그런데 파라미터 바인딩은 대부분 클라이언트가 HTTP 요청 정보를 잘못 호출해서 발생하는 문제이다.
  • HTTP 에서는 이런 경우 HTTP 상태 코드 400을 사용하도록 되어 있다.
  • DefaultHandlerExceptionResolver 는 이것을 500 오류가 아니라 HTTP 상태 코드 400 오류로 변경한다.

ApiExceptionController - 추가

Integer data 에 문자를 입력하면 내부에서 TypeMismatchException 이 발생한다.

실행 결과

정상적으로 입력했을 경우

비정상적으로 입력했을 경우

실행 결과를 보면 HTTP 상태 코드가 400인 것을 확인할 수 있다.


API 예외 처리 - @ExceptionHandler

HTML 화면 오류 vs API 오류

웹 브라우저에 오류가 발생했을 때 BasicErrorController 를 사용해 단순한 방식으로 5xx, 4xx 관련된 오류 화면을 보여준다.. BasicErrorController 는 이런 메커니즘으로 동작한다.

하지만 API 는 각 시스템 마다 하나하나 전부 출력해야하는 번거로움이 있다. 그리고 같은 예외라고 해도 어떤 컨트롤러에서 발생했는가에 따라서 다른 예외 응답을 내려주어야 할 수 있다.

지금까지 살펴봤을 때 BasicErrorController 를 사용하거나 HandlerExceptionResolver 를 직접 구현하는 방식으로 API 예외를 다루기는 쉽지 않다. 복잡하고 번거롭고 혹은 단순하게 만들 수 있으나 세부사항을 입력하려면 여러가지 로직을 추가야 해야 하는 등...

스프링에서 이런 단점들을 보안해서 나온게 @ExceptionHandler 이다.

@ExceptionHandler

스프링은 API 예외 처리 문제를 해결하기 위해 @ExceptionHandler 라는 애노테이션을 사용하는 매우 편리한 예외 처리 기능을 제공하는데, 이것이 바로 ExceptionHandlerExceptionResolver 이다.

스프링은 ExceptionHandlerExceptionResolver 를 기본으로 제공하고, 기본으로 제공하는 ExceptionResolver 중에 우선순위도 가장 높다. 실무에서 API 예외 처리는 대부분 이 기능을 사용한다.

예제를 통해 어떤 식으로 흘러가는지 확인해보자.

ErrorResult - 신규 클래스 생성

예외가 발생했을 때 API 응답으로 사용하는 객체를 정의했다.

ApiExceptionV2Controller - 신규 클래스 생성

우선순위

스프링의 우선순위는 항상 자세한 것이 우선권을 가진다. 예를 들어서 부모, 자식 클래스가 있고 다음과 같이 예외가 처리된다.

  • 부모 클래스는 자식 클래스까지 처리할 수 있다.
  • 자식예외 가 발생하면 부모예외처리(), 자식예외처리() 둘다 호출 대상이 된다.
  • 둘 중 더 자세한 것이 우선권을 가지므로 자식예외처리() 가 호출된다.
  • 부모예외 가 호출되면 부모예외처리() 만 호출 대상이 되므로 부모예외처리() 가 호출된다.

다양한 예외

다음과 같이 다양한 예외를 한번에 처리할 수 있다.

예외 생략

@ExceptionHandler 에 예외를 생략할 수 있다. 생략하면 메서드 파라미터의 예외가 지정된다.

파리미터와 응답

@ExceptionHandler 에는 마치 스프링의 컨트롤러의 파라미터 응답처럼 다양한 파라미터와 응답을 지정할 수 있다.

IllegalArgumentException 처리

실행 흐름

  • 컨트롤러를 호출한 결과 IllegalArgumentException 예외가 컨트롤러 밖으로 던져진다.
  • 예외가 발생했으로 ExceptionResolver 가 작동한다. 가장 우선순위가 높은 ExceptionHandlerExceptionResolver 가 실행된다.
  • ExceptionHandlerExceptionResolver 는 해당 컨트롤러에 IllegalArgumentException 을 처리할 수 있는 @ExceptionHandler 가 있는지 확인한다.
  • illegalExHandle() 를 실행한다. @RestController 이므로 illegalExHandle() 에도 @ResponseBody 가 적용된다. 따라서 HTTP 컨버터가 사용되고, 응답이 다음과 같은 JSON으로 반환된다.
  • @ResponseStatus(HttpStatus.BAD_REQUEST) 를 지정했으므로 HTTP 상태 코드 400으로 응답한다.

bad - 실행 결과

위와 같이 출력된다면 오류 없이 제대로 처리된 것 이다.


API 예외 처리 - @ControllerAdvice

@ControllerAdvice 또는 @RestControllerAdvice 를 사용해 정상 코드와 예외 처리 코드가 하나의 컨트롤러에 있는걸 깔끔하게 둘로 나눌 수 있다.

ApiExceptionV2Controller 코드에 있는 @ExceptionHandler 코드 복사 후 모두 제거

복사한 @ExceptionHandler 코드 새로 생성한 ExControllerAdvice 클래스에 복사 붙이기

이대로 실행해서 정상작동 하는지 테스트.

마지막으로 글로벌로 테스트할 수 있는지도 확인

ApiExceptionV2Controller 복사 후 ApiExceptionV3Controller 신규 생성

ExControllerAdvice - 수정

@RestControllerAdvice(basePackages = "hello.exception.api") 만 추가해주고 작동하는지 테스트.

정상적으로 작동한다. 끝

profile
안녕하세요

0개의 댓글