HTML 페이지의 경우 오류 페이지만 생성해놓으면 쉽게 해결할 수 있었지만 API는 단순히 고객에게 오류 화면을 노출시키는 것이 아니라 각 오류 상황에 맞는 오류 응답 스펙을 정하고, JSON으로 데이터를 내려줘야 한다.
예외가 발생해도 json으로 응답해줘야 하는데 웹 브라우저로 호출한 것이 아님에도 불구하고 html이 돌아온다.
클라이언트는 정상 요청이든, 오류 요청이든 JSON이 반환되기를 기대한다.
produces = MediaType.APPLICATION_JSON_VALUE
: 클라이언트가 요청하는 HTTP Header의 Accept 값이 application/json일 때 해당 메소드 호출
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
를 사용하자
목표: 예외가 발생해 서블릿을 넘어 WAS까지 예외가 전달되면 500처리. 이를 다른 상태코드로 처리하고 싶다.
또한 오류 메시지, 형식등 API별로 다르게 처리하고 싶다.
📌 4xx
: 클라이언트 잘못
5xx
: 서버 잘못
HandlerExceptionResolver
스프링 MVC는 컨트롤러(핸들러) 밖으로 예외가 던져진 경우 예외를 해결하고, 동작을 새로 정의할 수 있는 방법을 제공한다.
컨트롤러 밖으로 던져진 예외를 해결하고 동작 방식을 변경하고 싶다면 이걸 이용하자.
✔️ 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로 바꿔치기)
📌 주의
configureHandlerExceptionResolvers()
는 스프링이 기본으로 제공하는 ExceptionResolver를 제거하므로 extendHandlerExceptionResolvers
사용하기
지금까지 예외가 처리되는 과정은 WAS까지 예외가 던져지고 다시 오류 페이지 정보를 찾아 컨트롤러를 호출하고.. 왔다갔다 매우 번거로웠다.
ExceptionResolver
를 활용해 예외 발생시 한 번에 처리하자.
ExceptionResolver
를 사용하면 컨트롤러에서 예외가 발생해도 ExceptionResolver에서 예외를 처리한다.
따라서 예외 발생시 서블릿 컨테이너까지 예외가 전달되지 않고 스프링 MVC에서 예외 처리가 끝이 난다. 결과적으로는 WAS 입장에서 정상처리가 된 것이다.
하지만 직접 ExceptionResolver
를 구현하니 복잡하다. 이제 스프링이 제공하는 ExceptionResolver
를 사용하자.
스프링부트가 기본으로 제공하는
ExceptionResolver
HandlerExceptionResolverComposite
에 다음 순서로 등록
1. ExceptionHandlerExceptionResolver
2. ResponseStatusExceptionResolver
3. DefaultHandlerExceptionResolver (가장 낮은 우선순위)
(이 세가지는 기본적으로 등록이 되어있음)ExceptionHandlerExceptionResolver에서 처리 안되고 null을 반환하면 2번 .. 3번 ..
1. ExceptionHandlerExceptionResolver
@ExceptionHandler
처리
⭐️ API 예외 처리는 대부분 이 기능으로 해결
2. ResponseStatusExceptionResolver
HTTP 상태 코드 지정
3. DefaultHandlerExceptionResolver
스프링 내부 기본 예외 처리
예외에 따라 HTTP 상태 코드를 지정해주는 역할
@ResponseStatus
가 달린 예외 처리ResponseStatusException
예외 처리✔️ BadRequestException.java
BadRequestException 예외가 컨트롤러 밖으로 넘어가면 ResponseStatusExceptionResolver예외가 해당 애노테이션을 확인해 오류 코드를 HttpStatus.BAD_REQUEST(400)으로 변경해 메세지를 담음.
결국 sendError(400)을 호출해 WAS에서 다시 오류 페이지를 요청한다는 것.
@ResponseStatus
는 개발자가 직접 변경할 수 없는 (애노테이션을 직접 넣어야 하는데 개발자가 코드를 수정할 수 없는 라이브러리의 예외 코드 같은 곳에는 적용 불가) 예외에는 적용 X
추가로 애노테이션을 사용하기 때문에 조건에 따라 동적으로 변경하는 것 또한 어렵다.
➡️ ResponseStatusException
예외로 해결
BadRequestException: 상태코드와 오류 메시지까지 한번에 해결
: 스프링 내부에서 발생하는 스프링 예외 해결
대표적으로 파라미터 바인딩 시점 타입이 맞지 않으면 내부에서 TypeMismatchException이 발생하고, 예외가 발생했기 때문에 가만히 두면 서블릿 컨테이너까지 오류가 올라가 500
오류가 발생한다.
그런데 사실 파라미터 바인딩은 대부분 클라이언트가 HTTP 요청 정보를 잘못 호출했기 때문에 발생하는 것이라 400
오류가 발생하는 것이 맞다.
DefaultHandlerExceptionResolver
는 이것을 내부에서 500
→ 400
으로 바꾸어준다.
DefaultHandlerExceptionResolver.handleTypeMismatch
를 보면 결국 ⭐️response.sendError()
로 문제를 해결한다.
다음은 타입 에러로 발생한 postman 결과인데 상태코드가 400으로 원하는 결과를 얻은 것을 확인할 수 있다.
정리
HandlerExceptionResolver
를 직접 사용하기에는 복잡하고 API 오류 응답의 경우 response에 직접 데이터를 넣어야 해 불편하다.
ModelAndView를 반환해야 하는 것도 API에는 잘 맞지 않는다.그래서 스프링은
@ExceptionHandler
라는 진~~~짜 좋은 예외 처리 기능을 제공한다.
웹 브라우저에 HTML 화면을 제공할 땐 오류가 발생하면 BasicErrorController
를 사용하는 것이 편하다.
단순히 400.html
, 500.html
... 이런 html 파일을 만들어 사용자에게 띄우면 되며 이는 BasicErrorController
가 제공해주기 때문이다.
그런데 API는 각 시스템마다 응답의 모양, 스펙 모두 다르다. 예외에 따라 각 다른 데이터를 출력해야 할 수도 있다.
같은 예외라고 해도 어떤 컨트롤러에서 발생했는가에 따라 다른 예외 응답을 내려주어야 할 때도 있다.
결국 API 예외를 다루기에는 BasicErrorController
는 공통으로 하나만 만들 수 있는 느낌이고 HandlerExceptionResolver
는 직접 구현해야해 번거롭고 쉽지 않다.
HandlerExceptionResolver
를 떠올려보면 API응답에는 필요없는 ModelAndView를 반환했다.
API 응답을 위해서는 HttpServletResponse
에 직접 응답 데이터를 넣어주었고 이 방법은 너무 불편하다.
이러한 불편함을 한꺼번에 없애는 ⭐️@ExceptionHandler
에 대해서 알아보자.
스프링은 API 예외 처리 문제를 해결하기 위해 이 애노테이션을 사용하는 매우 편리한 예외 처리 기능을 제공한다. ➡️ ExceptionHandlerExceptionResolver
이는 기본으로 제공하는 ExceptionResolver 중 우선 순위도 가장 높다.
그리고 무엇보다 실무 API 예외 처리 기능은 대부분 이를 이용한다.
@ExceptionHandler
애노테이션 선언📌 지정한 예외 또는 그 예외의 자식 클래스는 모두 잡을 수 있음
(IllegalArgumentException, UserException은 각각 자식 오류까지 처리할 수 있으며, Exception은 최상위 클래스기 때문에 이 둘이 잡지 못한 에러면 다 처리할 수 있다.)
📌 스프링은 항상 구체적인 것이 우선순위를 가진다.
📌 @ExceptionHandler
에 예외를 생략할 수 있는데 생략하면 메소드 파라미터의 예외가 지정된다.
실행 흐름
- 컨트롤러 호출 결과
IllegalArgumentException
예외가 컨트롤러 밖으로 던져짐- 예외가 발생했으므로
ExceptionResolver
작동 → 가장 우선순위가 높은ExceptionHandlerExceptionResolver
실행ExceptionHandlerExceptionResolver
는 해당 컨트롤러에IllegalArgumentException
을 처리할 수 있는@ExceptionHandler
가 있는지 확인illegalExHandle()
실행
@RestController
이므로illegalExHandle()
에도@ResponseBody
적용
따라서 HTTP 컨버터가 사용되고 응답이 JSON으로 반환@ResponseStatus(HttpStatus.BAD_REQUEST)
를 지정했으므로 400응답
ResponseEntity
: HTTP 응답 코드를 프로그래밍하여 동적으로 변경할 수 있음
(@ResponseStatus
는 애노테이션이므로 HTTP 응답 코드를 동적으로 변경할 수 없음)
ModelAndView
로 오류 화면 HTML을 응답하는데 사용할 수도 있음
목표: 정상 코드와 예외 처리 코드를 분리하자
➡️ @ControllerAdvice
or @RestControllerAdvice
사용
대상으로 지정한 여러 컨트롤러에 @ExceptionHandler
, @InitBinder
기능을 부여해줌
대상을 지정하지 않으면 모든 컨트롤러에 적용(글로벌)
@RestControllerAdvice
는 @ControllerAdvice
와 같고, @ResponseBody
가 추가되어 있음
(@Controller
, @RestController
의 차이와 같음)
즉,
@ExceptionHandler
,@ControllerAdvice
를 조합하면 예외 처리를 깔끔하게 해결할 수 있다.