-
String MVC - API 예외처리(ExceptionHandlerExceptionResolver, ResponseStatusExceptionResolver, DeFaultHandlerExceptionResolver)Spring/Spring MVC 2022. 2. 14. 18:17728x90반응형
- ResponseStatusExceptionResolver
- DefaultHandlerExceptionResolver
- ExceptionHandlerExceptionResolver
- @ControllerAdvice, @RestControllerAdvice
API 예외처리
스프링 부트가 기본으로 제공하는 ExceptionResolver는 다음과 같다.
ExceptionResolver 설명 우선 순위 ExceptionHandlerExceptionResolver @ExceptionHandler 처리 1 ResponseStatusExceptionResolver 상태 코드가 적용된 예외 처리 2 DefaultHandlerExceptionResolver 스프링 내부 기본 예외 처리 3 ResponseStatusExceptionResolver
ResponseStatusExceptionResolver는 예외에 따라서 HTTP 상태 코드를 지정해주는 역할을 한다. 예외가 컨트롤러 밖으로 넘어가면 ResposeStatusExceptionResolver가 @ResponseStatus가 달려있는 예외를 찾고 오류 코드와 메시지를 담아준다.
@ResponseStatus를 예외에 적용
- ResponseStatusExceptionResolver가 처리할 수 있도록 예외 클래스에 @ResponseStatus를 적용
- 오류 코드와 메시지를 @ResponseStatus를 이용해 예외에 담아줄 수 있다
- ResponseStatusExceptionResolver 내부에서 response.sendError를 호출한다.
- sendError를 호출했기 때문에 WAS에서 다시 오류 페이지를 내부 요청한다.
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "error.bad") public class BadRequestException extends RuntimeException { }
▶ BadRequestException 예외가 발생 시 ResponseStatusExceptionResolver 해당 예외를 찾아 처리
▶ sendError(HttpStatis.BAD_REQUEST, "잘못된 요청 오류")를 호출
▶ reason에 메시지 기능을 사용할 수 있다.
ResponseStatusException
- 개발자가 직접 코드를 수정할 수 없는 라이브러리의 예외에 적용하기 위해 쓰인다.
- 조건에 따른 동적 변경이 가능하다.
@GetMapping("/api/response-status-ex2") public String responseStatusEx2() { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new IllegalArgumentException()); }
▶ ResponseStatusException 클래스를 생성하여 상태 정보, 에러 메시지, 예외 객체를 넘겨서 사용한다.
DefaultHandlerExceptionResolver
DefaultHandlerExceptionResolver는 스프링 내부에서 발생하는 스프링 예외를 해결한다. 대표적으로 파라미터 바인딩 시점에 타입이 맞지 않으면 내부에서 TypeMismatchException이 발생하는데, 이 경우 해당 예외가 서블릿 컨테이너까지 예외가 올라가고, 결과적으로 500 오류가 발생한다. 그런데 파라미터 바인딩은 대부분 클라이언트의 HTTP 요청 정보를 잘못 호출해서 발생하는 문제이다. 때문에 HTTP 상태 코드를 400으로 변경해야 하는데 DefaultHandlerExceptionResolver가 500 오류가 아닌 HTTP 상태 코드 400 오류로 변경해준다.
DefaultHandlerExceptionResolver 클래스
- DefaultHandlerExceptionResolver 클래스 내부의
doResolveException 메서드를 보면 처리해주는 예외들을 볼 수 있다. - 해당 코드를 더 살펴보면 sendError를 호출하는 것을 볼 수 있다.
- sendError를 호출했기 때문에 WAS에서 내부 요청이 발생
@Override @Nullable protected ModelAndView doResolveException( HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) { try { if (ex instanceof HttpRequestMethodNotSupportedException) { return handleHttpRequestMethodNotSupported( (HttpRequestMethodNotSupportedException) ex, request, response, handler); } else if (ex instanceof HttpMediaTypeNotSupportedException) { return handleHttpMediaTypeNotSupported( (HttpMediaTypeNotSupportedException) ex, request, response, handler); } ... }
ExceptionHandlerExceptionResolver
HTML 화면을 제공할 때는 오류가 발생하면 BasicErrorController를 사용하는게 편리하지만 API는 각 시스템마다 응답을 다르게 처리해야 한다. 예외 상황에 대해 단순히 오류 화면을 보여주는 것이 아니라 예외에 따라 각각 다른 데이터를 출력해야 할 수 도 있다. 또한 같은 예외라고 해도 어떤 컨트롤러에서 발생했는지에 따라 서로 다른 예외 응답을 전달해줘야 하는 경우게 생긴다. 즉, API 오류는 한마디로 매우 세밀한 제어가 필요하다.
HandlerExceptionResolver를 이용하여 오류에 관한 응답 정보들을 HttpServletResponse에 직접 넣어 방식을 사용하면 각 오류에 대해 일일이 작성해주기 번거로움이 생긴다. 이러한 번거로움을 해결하기 위해 @ExceptionHandler라는 애노테이션을 사용하는 ExceptionHandlerExceptionResolver를 제공한다.
예시를 위해 컨트롤러에 이너 클래스 생성
@Data @AllArgsConstructor public class ErrorResult { private String code; private String message; }
@ExceptionHandler 예외 처리 방법 1 ( 해당 컨트롤러에서 처리 )
- @ExceptionHandler 애노테이션을 선언하고 해당 컨트롤러에서 처리하고 싶은 예외를 지정
- 해당 컨트롤러에서 예외가 발생하면 @ExceptionHandler 애노테이션이 붙은 메서드 호출
- @ResponseStatus를 통해 상태 코드 변경
(상태 코드를 변경하지 않으면 클라이언트에서는 200 상태 코드를 되므로 주의) - 예외 처리를 모두 완료된 상태이기 때문에 정상 흐름으로 진행되어 WAS의 내부호출이 발생하지 않는다.
ApiException2Controller
@ResponseStatus(HttpStatus.BAD_REQUEST) // 예외 상태 코드 @ExceptionHandler(IllegalArgumentException.class) public ErrorResult illeegaExhandler(IllegalArgumentException e) { log.error("[exceptionHandler] ex", e); return new ErrorResult("BAD", e.getMessage()); }
@ExceptionHandler 예외 처리 방법 2
- @ExceptionHandler 파라미터의 예외를 생략할 수 있다.
(생략 시 메서드 파라미터의 예외가 지정)
@ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler // 파라미터와 동일하다면 (UserException.class) 생략 가능 public ResponseEntity<ErrorResult> illeegaExhandler(UserException e) { log.error("[exceptionHandler] ex", e); ErrorResult errorresult = new ErrorResult("USER-EX", e.getMessage()); return new ResponseEntity(errorResult, HttpStatus.BAD_REQUEST); }
@ExceptionHandler 예외 처리 방법 3
- 다양한 예외를 한번에 처리할 수 있다.
@ExceptionHandler({AException.class, BException.class}) public String ex(Exception e) { log.info("exception e", e); return new ErrorResult("EXS", e.getMessage()); }
@ExceptionHandler 예외 처리 방법 4
- 부모, 자식 예외 클래스가 존재할 경우
- 부모 예외 클래스 : 자식 예외 클래스까지 처리할 수 있다.
- 자식 예외 클래스 : 자식 예외가 발생하면 우선권을 갖는다.
@ExceptionHandler(부모예외.class) public String 부모예외처리()(부모예외 e) {} @ExceptionHandler(자식예외.class) public String 자식예외처리()(자식예외 e) {}
@ExceptionHandler 파라미터
- 다양한 파라미터를 통해 응답을 지정할 수 있다.
- 참고 사이트 (1.3.6. Exceptions)
Web on Servlet Stack (spring.io)
예외 처리 흐름
- 핸들러에서 IllegalArgumentException 예외 발생
- 스프링은 ExceptionResolver의 우선순위가 가장 높은 ExceptionHandlerExceptionResolver가 실행
- ExceptionHandlerExceptionResolver는 해당 컨트롤러에서 IllegalArgumentException 예외를
처리할 수 있는 @ExceptionHandler가 있는지 확인 - @ExceptionHandler를 찾으면 해당 메서드 실행
- 예외 처리를 완료하여 WAS는 정상 흐름으로 진행
(내부 호출하지 않는다)
@ControllerAdvice, @RestControllerAdvice
@ExceptionHandler를 사용하면 예외를 쉽게 처리할 수 있게 되었지만 핸들러와 예외 처리 메서드들이 섞여 있어 유지 보수 관점에서 좋지 않고 해당 컨트롤러에서만 적용이 된다는 단점이 존재한다. 그렇기 때문에 스프링에서 제공하는 @ControllerAdvice와 @RestControllerAdvice를 이용해 예외 처리 메서드들을 분리하여 관리한다.
@ControllerAdvice
- 컨트롤러를 지정하여 @ExceptionHandler와 @InitBinder 기능을 부여해준다.
- 컨트롤러를 지정하지 않으면 모든 컨트롤러에 적용된다.(글로벌)
- @RestControllerAdvice는 @ControllerAdvice + @ResponseBody 와 같다.
ExceptionControllerAdvice
@Slf4j @RestControllerAdvice(basePackages = "hello.exception.api") public class ExControllerAdvice { @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(IllegalArgumentException.class) public ErrorResult illegalExHandler(IllegalArgumentException e) { log.error("[exceptionHandler] ex", e); return new ErrorResult("BAD", e.getMessage()); } @ExceptionHandler public ResponseEntity<ErrorResult> userExHandler(UserException e) { log.error("[exceptionHandler] ex", e); ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage()); return new ResponseEntity(errorResult, HttpStatus.BAD_REQUEST); } @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler public ErrorResult exHandler(Exception e) { log.error("[exceptionHandler] ex", e); return new ErrorResult("EX", "내부 오류"); } }
▶ @RestControllerAdvice(basePackages = "hello.exception.api")
- hello.exception.api 패키지의 모든 컨트롤러에 적용
대상 컨트롤러 지정 방법
// 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 {}
728x90반응형'Spring > Spring MVC' 카테고리의 다른 글
Spring MVC - Formatter 구현 및 사용 ( Formatter와 FormattingConversionService ) (0) 2022.02.17 Spring MVC - Converter 구현 및 사용 (Converter와 ConversionService) (0) 2022.02.17 String MVC - 직접 구현해보는 API 예외처리 (0) 2022.02.14 Spring MVC - BasicErrorController를 이용한 HTML 오류 페이지 처리와 오류 정보 이용하기(text/html) (0) 2022.02.10 Spring MVC - 서블릿 예외(Exception) 에 따른 필터(Filter)와 인터셉터(Interceptor) 처리 (0) 2022.02.10