[Spring MVC] [2] 9. API 예외 처리

윤경·2021년 9월 28일
0

Spring MVC

목록 보기
24/26
post-thumbnail

[1] 시작

HTML 페이지의 경우 오류 페이지만 생성해놓으면 쉽게 해결할 수 있었지만 API는 단순히 고객에게 오류 화면을 노출시키는 것이 아니라 각 오류 상황에 맞는 오류 응답 스펙을 정하고, JSON으로 데이터를 내려줘야 한다.

예외가 발생해도 json으로 응답해줘야 하는데 웹 브라우저로 호출한 것이 아님에도 불구하고 html이 돌아온다.
클라이언트는 정상 요청이든, 오류 요청이든 JSON이 반환되기를 기대한다.

produces = MediaType.APPLICATION_JSON_VALUE: 클라이언트가 요청하는 HTTP Header의 Accept 값이 application/json일 때 해당 메소드 호출


[2] 스프링 부트 기본 오류 처리

API 예외 처리도 스프링 부트가 제공하는 기본 오류 방식을 사용할 수 있다.

✔️ BasicErrorController.java
errorHtml() - produces = MediaType.TEXT_HTML_VALUE: 클라이언트 요청 Accept 헤더 값이 text/html일 때 errorHtml() 호출해 view 제공
error(): 위 외의 경우 호출돼 ResponseEntity로 HTTP Body에 JSON 데이터 반환

그렇지만 저번에 얘기했듯 사용자에게 오류 메시지들을 마구잡이로 보여주는 것은 위험하므로 노출시키지 않는 편이 좋다.

Html 페이지 VS API 오류
참고로 BasicErrorController를 확장하면 JSON 메시지도 변경할 수 있다. (이렇게 이해만 해두자 더 좋은 방법이 있다.)

스프링 부트가 제공하는 BasicErrorController는 HTML 페이지를 제공하는 경우에는 매우 편리하지만 API 오류 처리는 다른 차원의 이야기이다. API는 각각 컨트롤러나 에러마다 서로 다른 응답 결과를 출력해야 할 수도 있다.

결론: API 오류처리는 @ExceptionHandler를 사용하자


[3] HandlerExceptionResolver 시작

목표: 예외가 발생해 서블릿을 넘어 WAS까지 예외가 전달되면 500처리. 이를 다른 상태코드로 처리하고 싶다.
또한 오류 메시지, 형식등 API별로 다르게 처리하고 싶다.

📌 4xx: 클라이언트 잘못
5xx: 서버 잘못

HandlerExceptionResolver
스프링 MVC는 컨트롤러(핸들러) 밖으로 예외가 던져진 경우 예외를 해결하고, 동작을 새로 정의할 수 있는 방법을 제공한다.

컨트롤러 밖으로 던져진 예외를 해결하고 동작 방식을 변경하고 싶다면 이걸 이용하자.

ExceptionResolver 적용 전

ExceptionResolver 적용 후

✔️ MyHandlerExceptionResolver.java

  • ExceptionResolver가 ModelAndView를 반환하는 이유는 마치 try, catch 하듯 Exception을 처리해 정상 흐름처럼 변경하려는 것이다. (이름 그대로 예외 해결사)

여기서는 IllegalArgumentException이 발생하면 response.sendError(400)을 호출해 HTTP 상태 코드를 400으로 지정하고 빈 ModelAndView를 반환한다.
Exception을 sendError로 바꿔치기 함

  • 반환 값에 따른 동작 방식
    빈 ModelAndView: new ModelAndView()처럼 빈 ModelAndView를 반환하면 뷰를 렌더링하지 않고 정상 흐름으로 서블릿이 리턴된다.
    ModelAndView 지정: View, Model 등의 정보를 지정해 반환하면 뷰를 렌더링
    null: null을 반환하면 다음 ExceptionResolver를 찾아 실행.
    만약 처리할 수 있는 ExceptionResolver가 없으면 예외 처리가 안되고 기존에 처리한 예외를 서블릿 밖으로 던진다.

  • ExceptionResolver
    - 예외 상태 코드 변환(sendError로 바꿔치기)

    • 뷰 템플릿 처리(새로운 오류 화면 뷰 렌더링해 클라이언트에게 제공)
    • API 응답 처리

📌 주의
configureHandlerExceptionResolvers()는 스프링이 기본으로 제공하는 ExceptionResolver를 제거하므로 extendHandlerExceptionResolvers 사용하기


[4] HandlerExceptionResolver 활용

지금까지 예외가 처리되는 과정은 WAS까지 예외가 던져지고 다시 오류 페이지 정보를 찾아 컨트롤러를 호출하고.. 왔다갔다 매우 번거로웠다.
ExceptionResolver를 활용해 예외 발생시 한 번에 처리하자.

ExceptionResolver를 사용하면 컨트롤러에서 예외가 발생해도 ExceptionResolver에서 예외를 처리한다.

따라서 예외 발생시 서블릿 컨테이너까지 예외가 전달되지 않고 스프링 MVC에서 예외 처리가 끝이 난다. 결과적으로는 WAS 입장에서 정상처리가 된 것이다.

하지만 직접 ExceptionResolver를 구현하니 복잡하다. 이제 스프링이 제공하는 ExceptionResolver를 사용하자.


[5] 스프링이 제공하는 ExceptionResolver1

스프링부트가 기본으로 제공하는 ExceptionResolver

HandlerExceptionResolverComposite에 다음 순서로 등록
1. ExceptionHandlerExceptionResolver
2. ResponseStatusExceptionResolver
3. DefaultHandlerExceptionResolver (가장 낮은 우선순위)
(이 세가지는 기본적으로 등록이 되어있음)

ExceptionHandlerExceptionResolver에서 처리 안되고 null을 반환하면 2번 .. 3번 ..

1. ExceptionHandlerExceptionResolver
@ExceptionHandler처리
⭐️ API 예외 처리는 대부분 이 기능으로 해결

2. ResponseStatusExceptionResolver
HTTP 상태 코드 지정

3. DefaultHandlerExceptionResolver
스프링 내부 기본 예외 처리

ResponseStatusExceptionResolver

예외에 따라 HTTP 상태 코드를 지정해주는 역할

  • @ResponseStatus가 달린 예외 처리
  • ResponseStatusException 예외 처리

✔️ BadRequestException.java
BadRequestException 예외가 컨트롤러 밖으로 넘어가면 ResponseStatusExceptionResolver예외가 해당 애노테이션을 확인해 오류 코드를 HttpStatus.BAD_REQUEST(400)으로 변경해 메세지를 담음.

결국 sendError(400)을 호출해 WAS에서 다시 오류 페이지를 요청한다는 것.

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

추가로 애노테이션을 사용하기 때문에 조건에 따라 동적으로 변경하는 것 또한 어렵다.
➡️ ResponseStatusException 예외로 해결

즉,

BadRequestException: 상태코드와 오류 메시지까지 한번에 해결


[6] 스프링이 제공하는 ExceptionResolver2

DefaultHandlerExceptionResolver

: 스프링 내부에서 발생하는 스프링 예외 해결

대표적으로 파라미터 바인딩 시점 타입이 맞지 않으면 내부에서 TypeMismatchException이 발생하고, 예외가 발생했기 때문에 가만히 두면 서블릿 컨테이너까지 오류가 올라가 500 오류가 발생한다.

그런데 사실 파라미터 바인딩은 대부분 클라이언트가 HTTP 요청 정보를 잘못 호출했기 때문에 발생하는 것이라 400 오류가 발생하는 것이 맞다.

DefaultHandlerExceptionResolver는 이것을 내부에서 500400으로 바꾸어준다.

DefaultHandlerExceptionResolver.handleTypeMismatch를 보면 결국 ⭐️response.sendError()로 문제를 해결한다.

다음은 타입 에러로 발생한 postman 결과인데 상태코드가 400으로 원하는 결과를 얻은 것을 확인할 수 있다.

정리
HandlerExceptionResolver를 직접 사용하기에는 복잡하고 API 오류 응답의 경우 response에 직접 데이터를 넣어야 해 불편하다.
ModelAndView를 반환해야 하는 것도 API에는 잘 맞지 않는다.

그래서 스프링은 @ExceptionHandler라는 진~~~짜 좋은 예외 처리 기능을 제공한다.


[7] ⭐️ @ExceptionHandler

  • HTML 화면 오류 VS API 오류

웹 브라우저에 HTML 화면을 제공할 땐 오류가 발생하면 BasicErrorController를 사용하는 것이 편하다.
단순히 400.html, 500.html... 이런 html 파일을 만들어 사용자에게 띄우면 되며 이는 BasicErrorController가 제공해주기 때문이다.

그런데 API는 각 시스템마다 응답의 모양, 스펙 모두 다르다. 예외에 따라 각 다른 데이터를 출력해야 할 수도 있다.
같은 예외라고 해도 어떤 컨트롤러에서 발생했는가에 따라 다른 예외 응답을 내려주어야 할 때도 있다.

결국 API 예외를 다루기에는 BasicErrorController는 공통으로 하나만 만들 수 있는 느낌이고 HandlerExceptionResolver는 직접 구현해야해 번거롭고 쉽지 않다.

  • API 예외처리의 어려운 점

HandlerExceptionResolver를 떠올려보면 API응답에는 필요없는 ModelAndView를 반환했다.
API 응답을 위해서는 HttpServletResponse에 직접 응답 데이터를 넣어주었고 이 방법은 너무 불편하다.

이러한 불편함을 한꺼번에 없애는 ⭐️@ExceptionHandler에 대해서 알아보자.

@ExceptionHandler

스프링은 API 예외 처리 문제를 해결하기 위해 이 애노테이션을 사용하는 매우 편리한 예외 처리 기능을 제공한다. ➡️ ExceptionHandlerExceptionResolver

이는 기본으로 제공하는 ExceptionResolver 중 우선 순위도 가장 높다.
그리고 무엇보다 실무 API 예외 처리 기능은 대부분 이를 이용한다.

@ExceptionHandler 예외 처리 방법

  1. @ExceptionHandler 애노테이션 선언
  2. 해당 컨트롤러에서 처리하고 싶은 예외를 지정
  3. 해당 컨트롤러에서 예외가 발생하면 이 메소드가 호출

📌 지정한 예외 또는 그 예외의 자식 클래스는 모두 잡을 수 있음

(IllegalArgumentException, UserException은 각각 자식 오류까지 처리할 수 있으며, Exception은 최상위 클래스기 때문에 이 둘이 잡지 못한 에러면 다 처리할 수 있다.)

📌 스프링은 항상 구체적인 것이 우선순위를 가진다.

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

실행 흐름

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

ResponseEntity: HTTP 응답 코드를 프로그래밍하여 동적으로 변경할 수 있음
(@ResponseStatus는 애노테이션이므로 HTTP 응답 코드를 동적으로 변경할 수 없음)

ModelAndView로 오류 화면 HTML을 응답하는데 사용할 수도 있음


[8] @ControllerAdvice

목표: 정상 코드와 예외 처리 코드를 분리하자
➡️ @ControllerAdvice or @RestControllerAdvice 사용

@ControllerAdvice

대상으로 지정한 여러 컨트롤러에 @ExceptionHandler, @InitBinder 기능을 부여해줌

대상을 지정하지 않으면 모든 컨트롤러에 적용(글로벌)

@RestControllerAdvice@ControllerAdvice와 같고, @ResponseBody가 추가되어 있음
(@Controller, @RestController의 차이와 같음)

즉, @ExceptionHandler, @ControllerAdvice를 조합하면 예외 처리를 깔끔하게 해결할 수 있다.


profile
개발 바보 이사 중

0개의 댓글