지난 포스팅에서 HandlerExceptionResolver를 직접 구현하여 API예외를 처리하는 방법에 대해서 알아보았다. HandlerExceptionResolver를 활용하면 컨트롤러 밖으로 던져진 예외를 처리해 WAS에서 에러 페이지를 재요청하는 복잡한 과정을 피할 수 있었지만, 직접 HandlerExceptionResolver를 구현하는 것이 매우 번거로웠다.
이번 포스팅에서는 스프링에서 제공하는 HandlerExceptionResolver에 대해 알아보자.
스프링 부트가 제공하는 ExceptionResolver는 다음 3가지가 있다. 우선순위 순으로 보자면
@GetMapping("/api/response-status-ex1")
public String responseStatusEx1() {
throw new BadRequestException();
}
@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "error.bad")
public class BadRequestException extends RuntimeException{
}
error.bad=잘못된 요청 오류입니다. 메세지 사용
server.error.include-exception=true
server.error.include-message=always
server.error.include-stacktrace=always
server.error.include-binding-errors=always
@GetMapping("/api/response-status-ex2")
public String responseStatusEx2() {
// throw new ResponseStatusException("상태코드", "예외 메세지", "처리할 예외 종류");
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new IllegalArgumentException());
}
@GetMapping("/api/default-handler-ex")
public String defaultException(@RequestParam Integer data) {
return "ok";
}
Exception | HTTP Status Code |
---|---|
HttpRequestMethodNotSupportedException | 405 (SC_METHOD_NOT_ALLOWED) |
HttpMediaTypeNotSupportedException | 415 (SC_UNSUPPORTED_MEDIA_TYPE) |
HttpMediaTypeNotAcceptableException | 406 (SC_NOT_ACCEPTABLE) |
MissingPathVariableException | 500 (SC_INTERNAL_SERVER_ERROR) |
MissingServletRequestParameterException | 400 (SC_BAD_REQUEST) |
MissingServletRequestPartException | 400 (SC_BAD_REQUEST) |
ServletRequestBindingException | 400 (SC_BAD_REQUEST) |
ConversionNotSupportedException | 500 (SC_INTERNAL_SERVER_ERROR) |
TypeMismatchException | 400 (SC_BAD_REQUEST) |
HttpMessageNotReadableException | 400 (SC_BAD_REQUEST) |
HttpMessageNotWritableException | 500 (SC_INTERNAL_SERVER_ERROR) |
MethodArgumentNotValidException | 400 (SC_BAD_REQUEST) |
BindException | 400 (SC_BAD_REQUEST) |
NoHandlerFoundException | 404 (SC_NOT_FOUND) |
AsyncRequestTimeoutException | 503 (SC_SERVICE_UNAVAILABLE) |
@Data
@AllArgsConstructor
public class ErrorResult {
private String code;
private String massage;
}
@Slf4j
@RestController
public class ApiExceptionV2Controller {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandler(IllegalArgumentException e) {
log.error("[exceptionHandler] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
// 예외를 생략할 경우 메서드 파라미터의 예외가 지정됨
@ExceptionHandler // (UserException.class)
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", "내부 오륲");
}
@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);
}
@Data
@AllArgsConstructor
static class MemberDto {
private String memberId;
private String name;
}
}
@ExceptionHandler를 이용해 컨트롤러에서 처리하고 싶은 예외를 지정해줄 수 있다.
또한 @ExceptionHandler에서 지정한 예외의 경우 그 자식 클래스에 속한 예외도 같이 처리해준다.
스프링의 경우 기본적으로 자세한 것이 높은 우선권을 가지기 때문에 부모 자식 관계에 있는 클래스의 경우 자식 클래스가 높은 우선권을 가지게 된다.
위의 코드를 보면
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandler(IllegalArgumentException e)
@ExceptionHandler
public ResponseEntity< ErrorResult> userExHandler(UserException e)
이처럼 @ExceptionHandler()의 파라미터로 예외를 명시하는 것과 안한 것이 있는 것을 확인할 수 있는데, @ExceptionHandler()의 파라미터를 생략할 경우 메서드 파라미터에서 지정한 예외가 @ExceptionHandler에서 처리하는 예외로 지정이 된다.
@ExceptionHandler(ViewException.class)
public ModelAndView ex(ViewException e) {
log.info("exception e", e);
return new ModelAndView("error");
}
// 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 {}
@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 // (UserException.class)
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", "내부 오륲");
}
}
@Slf4j
@RestController
public class ApiExceptionV3Controller {
@GetMapping("/api3/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);
}
@Data
@AllArgsConstructor
static class MemberDto {
private String memberId;
private String name;
}
}
이것으로 예외 처리에 대해서 알아보았다. 하지만 실무에서 적절하게 활용하기 위해서는 많이 사용해봐야 할 것 같다.
출처 : 김영한 스프링MVC2편