디스패처 서블릿에서 원래 예외가 발생하면 postHandler는 동작하지 않고 afterCompletion이 동작후 WAS로 예외를 전달하게 됨
하지만 ExceptionResolver를 사용하면 컨트롤러에서 발생한 예외에 대한 처리 로직이 추가될 수 있음
물론 이때도 postHandler는 동작하지 않음
ExceptionResolver 구현
@Slf4j
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
try {
if (ex instanceof IllegalArgumentException) {
log.info("IllegalArgumentException resolver to 400");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
return new ModelAndView();
}
} catch (IOException e) {
log.error("resolver ex", e);
}
return null;
}
}
HandlerExceptionResolver를 구현하면 컨트롤러에서 발생한 에러가 넘어와 처리가 가능함
위 예제는 IllegalArgumentException에 대해 에러 코드를 400으로 바꿔줌
ModelAndView를 반환하면 try - catch를 한 것 처럼 에러를 잡고 정상 흐름 처럼 변경함
만약 null을 반환하면 다른 ExceptionResolver를 찾거나 예외를 서블릿 밖으로 던짐
구현 이후 config에 등록을 해 줘야함
스프링이 제공하는 ExceptionResolver
스프링 부트가 기본으로 제공하는 ExceptionResolver 는 다음과 같음
HandlerExceptionResolverComposite 에 다음 순서로 등록
ExceptionHandlerExceptionResolver: @ExceptionHandler를 처리하며 대부분 이 기능으로 해결
ResponseStatusExceptionResolver: HTTP 상태 코드를 지정해줌
DefaultHandlerExceptionResolver: 스프링 내부 기본 예외를 처리하며 우선 순위가 가장 낮음
ResponseStatusExceptionResolver
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
public class BadRequestException extends RuntimeException {
}
BadRequsetException 예외가 컨트롤러 밖으로 넘어가면 ResponseStatusExceptionResolver 가 해당 어노테이션을 확인해 오류코드를 400으로 변경하고 메시지도 남김
ResponseStatusExceptionResolver는 내부에서 response.sendError(statusCode, resolvedReason)을 호출함
DefaultHandlerExceptionResolver
파라미터 바인딩 시점에 타입이 맞지 않으면 내부에서 TypeMismatchException이 발생
이 경우 예외가 발생했기 때문에 그냥 두면 서블릿 컨테이너까지 예외가 올라가고 결과적으로 500 에러 발생
하지만, 파라미터 바인딩인 클라이언트가 요청을 잘못 보낸것이기 때문에 이런 경우엔 400 에러를 내려줘야 함
DefaultHandlerExceptionResolver는 이러한 오류에 대해 미리 정의를 해서 발견 후 400으로 변경해줌
ExceptionHandlerExceptionResolver 와 @ExceptionHandler
@Slf4j
@RestController
public class ApiExceptionV2Controller {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandle(Exception e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("EX", "내부 오류");
}
@GetMapping("/api2/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
if (id.equals("ex")) {
throw new RuntimeException("잘못된 사용자");
}
if (id.equals("bad")) {
throw new IllegalArgumentException("잘못된 입력 값");
}
if (id.equals("user-ex")) {
throw new UserException("사용자 오류");
}
return new MemberDto(id, "hello " + id);
}
}
스프링은 ExceptionHandlerExceptionResolver를 기본으로 제공하고, 우선순위도 가장 높음
실무에서는 대부분 이 기능을 사용해서 처리
@ExceptionHandler 어노테이션을 선언하고, 해당 컨트롤러 내부에서 처리하고 싶은 예외를 지정
예를 들어, illegalExHandler는 IllegalArgumentException와 그 하위에 대해 ResponseStatus를 400으로 변경하고 메시지를 지정해줄 수 있음
exHandle 함수는 Excpetion e를 인자로 받는데, 이는 모든 Exception을 처리할 수 있다는 의미.
하지만 IllegalArgumentExcpetion이 더 구체적인 예외이므로 IllegalArgumentExcpetion 예외가 발생할 경우에는 illegalExHandle이 호출 됨
더 구체적인 예외 처리가 우선
따라서 exHandle은 그 외 나머지에 대한 Default 처리를 위해 존재함
이 방식은 해당 컨트롤러에서만 동작함
@ControllerAdvice
@Slf4j
@RestControllerAdvice
public class ExControllerAdvice {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandle(Exception e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("EX", "내부 오류");
}
}
//대상컨트롤러 지정 방법
// 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 {}
@ExceptionHandler가 하나의 컨트롤러에서만 동작하는데, 이 @ExceptionHandler를 모아 여러 컨트롤러에서 동작할 수 있게 해줌