API 예외처리의 어려운 점
- HandlerExceptionResolver의 경우 ModelAndView를 반환하는데, 이는 API 응답에서는 불필요
- 또한, API 응답을 위해서 HttpServlerResponse에 직접 응답 데이터를 넣었지만, 과정이 불편
- 특정 컨트롤러에서만 발생하는 예외를 별도로 처리하기 어려움
ExceptionHandlerExceptionResolver
@ExceptionHandler({예외처리1.class, 예외처리2.class..})
Method 선언
- 스프링에서 기본 제공
- ExceptionResolver 중 우선순위가 가장 높음
- @ExceptionHandler 애노테이션을 선언하고, 해당 컨트롤러에서 처리하고 싶은 예외를 지정
- 지정한 예외 또는 그 예외의 자식 클래스는 모두 예외 처리 가능
- 다양한 예외를 한번에 처리 가능
- 예외 생략 시 Method 파라미터의 예외가 지정
- ModelAndView를 사용해서 오류 화면(HTML)을 응답하는데 사용 가능
사용 예시
- 예외가 발생했을 때 API 응답으로 사용하는 객체 정의
@Data
@AllArgsConstructor
public class ErrorResult {
private String code;
private String message;
}
- 컨트롤러
@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());
}
// 예외 생략 시 Method의 파라미터의 예외가 지정. 여기서는 UserException.class
@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","내부 오류");
}
// ModelAndView를 사용해서 오류 화면을 응답하는데 사용 가능
@ExceptionHandler(ViewException.class)
public ModelAndView ex(ViewException e) {
log.info("exception e", e);
return new ModelAndView("error");
}
@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(부모예외.class)
public String 부모예외처리()(부모예외 e) {}
@ExceptionHandler(자식예외.class)
public String 자식예외처리()(자식예외 e) {}
- 항상 자세한 것이 우선권을 가짐
- 자식예외가 발생하면 부모예외처리(), 자식예외처리() 둘 다 호출 대상
- 그러나 둘 중 더 자세한 자식예외처리()가 호출
- 물론 부모예외가 호출되면 부모예외처리()만 호출 대상이 되므로 부모예외처리()가 호출
실행 흐름
- IllegalArgumentException 처리
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
- 컨트롤러를 호출한 결과 예외가 발생하여 컨트롤러 밖으로 던져짐
- 예외 발생으로 ExceptionResolver 작동(우선순위가 높은 ExceptionHandlerExceptionResolver 실행)
- ExceptionHandlerExceptionResolver가 @ExceptionHandler가 있는지 확인
- @ExceptionHandler가 있는 illegalExHandle() 실행
- @RestController 이므로 @ResponseBody가 적용되어 HTTP 컨버터가 사용되고, 응답이 JSON으로 변환
- @ResponseStatus으로 인해 HTTP 상태 코드 400으로 응답
- UserException 처리
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(UserException e) {
log.error("[exceptionHandle] ex", e);
ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}
- @ExceptionHandler에 예외를 지정해주지 않았으므로 Method의 파라미터 예외를 사용
- ResponseEntity를 사용해서 HTTP 메시지 바디에 직접 응답
@ControllerAdvice, @RestControllerAdvice
- 모든 @Controller에 대해 예외를 잡아 처리
- Class 내의 해당 애노테이션 선언 후 @ExceptionHandler를 통해 핸들링하고 싶은 예외를 잡아서 처리 가능
- @ControllerAdvice는 대상으로 지정한 여러 컨트롤러에 @ExceptionHandler, @InitBinder 기능을 부여
- 정상 코드와 예외 처리 코드가 하나의 컨트롤러에 섞여 있는 것을 방지(코드 중복 제거 및 관심사 분리 가능)
- 에러 페이지로 리다이렉트를 원한다면 @ControllerAdvice
- 객체만 return 시키길 원한다면 @RestContollerAdvice
사용 예시
@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","내부 오류");
}
}
대상 컨트롤러 지정 방법
// 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 {}
- 대상을 지정하지 않으면 모든 컨트롤러에 적용(글로벌 적용)