Spring Boot의 예외 처리는 @RestControllerAdvice 를 사용한다.
@RestControllerAdvice = ControllerAdvice + ResponseBody
따라서, ResponseBody와 같이 리턴하는 응답 값이 body로 설정되어 클라이언트에게 전달된다.
아래와 같이 IndexOutOfBoundsException을 발생하도록 설정하고 HTTP Get요청을 보내면 아래 그림과 같이
Http Status Code 500번과 Internal Server Error이 반환된다.
@Slf4j
@RestController
@RequestMapping("/api")
public class RestApiBController {
@GetMapping(path = "")
public void hello() {
var list = List.of("hello");
var element = list.get(1);
log.info("element : {}", element);
}
}
예외가 발생해도 자연스럽게 응답을 내려주기 위해서는 서버에서 컨트롤러에 대한 예외를 처리해야 한다.
try catch로 직접 처리할 수 있지만 코드 길이가 길어질 수 있으므로 @RestControllerAdvice 를 사용한 Handler 클래스를 만들어서 사용한다.
@RestControllerAdvice 가 붙은 클래스는 Rest API Controller에서 예외가 일어나는 것을 감지
@ExceptionHandler로 어떠한 예외를 캐치할 것인지 선언 가능, 아래의 코드에선 (IndexOutOfBoundsException)을 value 값으로 설정했으므로 해당 예외를 처리한다.
@Slf4j
@RestControllerAdvice
//-> 아래 클래스는 rest api를 사용하는 곳에 에러를 감지함
public class RestApiExceptionHandler {
@ExceptionHandler(value = { IndexOutOfBoundsException.class })
public ResponseEntity outOfBound(
IndexOutOfBoundsException e
){
log.error("IndexOutOfBoundsException",e);
return ResponseEntity.status(200).build();
}
}
ResponseEntity를 반환하기에 HttpStatus 코드를 200으로 변경하여 반환하면 이전과 다르게 예외가 핸들링된 것을 볼 수 있다. @ExceptionHandler(value = { IndexOutOfBoundsException.class }) 의 value 값을 Exception.class로 바꾸게 되면 전체적인 Exception 하위의 예외들을 한 번에 처리할 수 있다.
다른 방법으로 따로 ExceptionHandler을 관리하지 않고, 컨트롤러 단에서 직접 @ExceptionHandler을 사용하여 해결할 수 있다. 아래 코드는 위와 같은 예제에서 Controller 내부에서 @ExceptionHandler 를 적용한 코드이다. 코드 길이가 길어질 수 있으므로 이 방법은 특별한 컨트롤러에서만 사용을 한다.
@Slf4j
@RestController
@RequestMapping("/api/b")
public class RestApiCController {
@GetMapping("")
public void hello() {
throw new NumberFormatException("number format exception");
}
@ExceptionHandler(value = NumberFormatException.class) //->글로벌로 가지 않고 여기서 해결
public ResponseEntity numberFormatException(
NumberFormatException e
){
log.error("RestApiCController", e);
return ResponseEntity.ok().build();
}
}
value 값에 클래스를 명시하는 것 이외에도 basePackages를 명시할 수 있다. @RestControllerAdvice 의 value값에 패키지 경로를 명시하게 되면 해당 패키지에 속한 컨트롤러에서 일어나는 모든 예외를 하나의 핸들러로 관리할 수 있다.
@Slf4j
@RestControllerAdvice(basePackages = "com.example.exception.controller") // 패키지 명시
@Order(1)
public class RestApiExceptionHandler {
@ExceptionHandler(value = { IndexOutOfBoundsException.class })
public ResponseEntity outOfBound(
IndexOutOfBoundsException e
){
log.error("IndexOutOfBoundsException",e);
return ResponseEntity.status(200).build();
}
@ExceptionHandler(value = NoSuchElementException.class)
public ResponseEntity<Api> noSuchElement(
NoSuchElementException e
){
log.error("",e);
var reponse = Api.builder()
.resultCode(String.valueOf(HttpStatus.NOT_FOUND))
.resultMessage(HttpStatus.NOT_FOUND.getReasonPhrase())
.build();
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(reponse);
}
}
ExceptionHandler가 2개 이상인 경우 @Order을 통해 먼저 처리될 수 있는, 우선순위를 부여할 수 있다.
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@Slf4j
@RestControllerAdvice(basePackages = "com.example.exception.controller") // 패키지 명시
@Order(1)
public class RestApiExceptionHandler {
@Order <<<< spec >>>>
int value() default Ordered.LOWEST_PRECEDENCE;
int LOWEST_PRECEDENCE = Integer.MAX_VALUE;
@Order의 우선순위를 설정할 때 기본 값은 LOWEST_PRECEDENCE = MAX_VALUE 로 설정되어 있다.
즉 @Order가 있을 경우, value가 낮은 친구 먼저 처리된다.
Order vs Order(1) : default가 MaxInt이므로 그보다 낮은 1이 실행
Order(2) vs Order(3) : 2가 실행
Order vs Order : Class 이름 순으로 실행
먼저 처리되고 싶으면 value에 낮은 값을, 나중에 처리되고 싶으면 높은 값을 주면 된다.
위의 GlobalExceptionHandler은 RestApiExceptionHandler에서 (처리하지 못한, 예상치 못한) 나머지 예외를 받아내기 위한 최후의 역할을 하므로RestApiExceptionHandler 의 Order(value)값 보다 높게 값을 설정하여 RestApiExceptionHandler가 먼저 처리되게 한다.
(위의 예시 상황은 Oder가 없는 경우 vs 있는 경우임... 만약 Order가 둘 다 있을 경우엔 값이 낮은 친구부터)