스프링 MVC 2편 - 백엔드 웹 개발 활용 기술을 공부하고 정리하는 포스트입니다.


🏷 API 예외 처리

API의 경우 어떻게 예외 처리를 할까

  • 오류 페이지는 단순히 고객에게 오류 화면을 보여주고 끝
    → API는 각 오류 상황에 맞는 오류 응답 스펙을 정하고, JSON으로 데이터를 내려줌

✔️ 스프링 부트 기본 오류 처리

  • BasicErrorController
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { }
  
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { }
  • errorHtml()
    • produce = MediaType.TEXT_HTML_VALUE :
      클라이언트 요청의 Accept 헤더 값이 text/html인 경우에는 errorHtml()을 호출해서 view를 제공
  • error
    • 그 외 경우 ResponseEntity로 HTTP Body에 JSON 데이터를 반환

✔️ HTML 페이지 vs API 오류

  • 스프링 부트가 제공하는 BasicErrorController는 HTML 페이지를 제공하는 경우에는 매우 편리
    → 4xx, 5xx 등등 모두 잘 처리해줌

  • 하지만 API 오류 처리는 다른 차원의 이야기!! → API 마다, 각각의 컨트롤러나 예외마다 서로 다른 응답 결과를 출력해야 할 수도 있음

    • 매우 세밀하고 복잡함
    • 따라서 이 방법은 HTML 화면을 처리할 때 사용하고, API는 오류 처리는 @ExceptionHandler를 사용!!

❗️ BasicErrorController를 확장해서 JSON 오류 메시지를 변경할 수 있다


🏷 HandlerExceptionResolver 활용

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



📌 참고
ExceptionResolver로 예외를 해결해도 postHandle()은 호출되지 않는다.


🏷 @ExceptionHandler

  • BasicErrorController를 사용하거나 HandlerExceptionResolver를 직접 구현하는 방식은 API 예외를 다루기 어려움

✔️ API 예외처리의 어려운 점

  • HandlerExceptionResolverModelAndView를 반환
    • API 응답에 필요하지 않음
  • API 응답을 하기 위해 HttpServletResponse에 직접 응답 데이터를 넣음
    • 매우 불편한 방식
  • 특정 컨트롤러에서만 발생하는 예외를 별도로 처리하기 어려움
    → 회원을 처리하는 컨트롤러와 상품을 관리하는 컨트롤러에서 각각 발생한 RuntimeException을 서로 다른 방식으로 처리하려면?

✔️ @ExceptionHandler

  • API 예외 처리 문제를 해결하기 위해 스프링이 제공하는 매우 편리한 기능이 바로 ExceptionHandlerExceptionResolver!
    • 스프링은 ExceptionHandlerExceptionResolver을 기본으로 제공
      ExceptionResolver 중에서 우선순위가 가장 높음
  • @ExceptionHandler 예외 처리 방법
    • @ExceptionHandler 애노테이션을 선언 → 해당 컨트롤러에서 처리하고 싶은 예외를 지정
    • 해당 컨트롤러에서 예외가 발생하면 이 메서드가 호출
    • 참고 : 지정한 예외와 그 예외의 자식 클래스 모두 잡음

📌 스프링의 우선순위는 항상 자세한 것이 우선권을 가진다.

실행 흐름

  • 예제 코드에서의 실행 흐름
  1. 컨트롤러가 호출한 결과 IllegalArgumentException 예외가 컨트롤러 밖으로 던져짐
  2. 예외 발생 → ExceptionResolver 작동. 가장 우선순위가 높은 ExceptionHandlerExceptionResolver가 실행
  3. ExceptionHandlerExceptionResolver는 해당 컨트롤러에 IllegalArgumentException을 처리할 수 있는 @ExceptionHandler가 있는지 확인
  4. illegalExHandler() 실행. @RestController이므로 @ResponseBody가 적용
  5. HTTP 컨버터 사용 → 응답이 JSON으로 반환
  6. @ResponseStatus(HttpStatus.BAD_REQUEST)가 지정되어 있으므로 HTTP 상태코드 400으로 응답

🏷 @ControllerAdvice

  • @ExceptionHandler를 사용해 예외를 깔끔하게 처리할 수 있게 되었지만, 정상 코드와 예외 처리 코드가 하나의 컨트롤러에 섞여 있음
    @ControllerAdvice 또는 @RestControllerAdvice를 사용해 분리 가능

✔️ ExControllerAdvice

  • @ControllerAdvice
    • @ControllerAdvice는 대상으로 지정한 여러 컨트롤러에 @ExceptionHandler, @InitBinder기능을 부여해주는 역할
    • @ControllerAdvice에 대상을 지정하지 않으면 모든 컨트롤러에 적용됨. (글로벌 적용)
  • @RestControllerAdvice
    • @ControllerAdvice와 같고, @ResponseBody가 추가
    • @Controller, @RestController의 차이와 동일

📌 대상 컨트롤러 지정 방법

// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class,
AbstractController.class})
public class ExampleAdvice3 {}

🌟 자세한 부분은 스프링 공식 문서 참고