HandlerExceptionResolver 인터페이스는 발생한 Exception을 catch하고 HTTP 상태나 응답 메세지 등을 설정한다.
HandlerExceptionResolver 는 적합한 예외 처리를 위해 HandlerExceptionResolver 구현체들을 빈으로 등록해서 관리한다.
DefaultErrorAttributes, ExceptionHandlerExceptionResolver, ResponseStatusExceptionResolver, DefaultHandlerExceptionResolver
DefaultErrorAttributes : 직접 예외를 처리하지 않고 속성만 관리하므로 나머지 3가지와 성격이 다르다.
나머지 3가지 : 직접 예외를 처리하는 ExceptionResolver (예외 처리기)
Spring 은 다음 도구들을 사용해서, ExceptionResolver를 동작시켜서 에러를 처리한다.
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class NoSuchElementFoundException extends RuntimeException {
...
}
/* 응답 결과
{
"timestamp": "2021-12-31T03:35:44.675+00:00",
"status": 404,
"error": "Not Found",
"path": "/product/5000"
}
*/
HTTP Status 와 Response 를 제공
ResponseStatusExceptionResolver 가 지정해준 상태로 에러 응답이 내려가도록 처리
Exception 클래스 자체
메소드에 @ExceptionHandler 와 함께
클래스에 @RestControllerAdvic e 와 함께
에러 응답의 내용(Message) 수정 X
(단, DefaultErrorAttributes를 수정하면 가능하긴 함)
예외 클래스와 강하게 결합돼서, 같은 예외는 같은 상태와 에러 메세지를 반환함
별도의 응답 상태가 필요하다면, 예외 클래스를 추가해야 됨
WAS까지 예외가 전달되고, WAS의 에러 요청 전달이 진행됨
외부에서 정의한 Exception 클래스에는 @ResponseStatus 를 붙여줄 수 없음
@GetMapping("/product/{id}")
public ResponseEntity<Product> getProduct(@PathVariable String id) {
try {
return ResponseEntity.ok(productService.getProduct(id));
} catch (NoSuchElementFoundException e) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Item Not Found");
}
}
코드를 외부 라이브러리에서 정의한 경우, 개발자는 해당 코드를 수정할 수 없으므로 @ResponseStatus 를 붙여줄 수 없다.
생성자로 HTTP Status 와 String 만을 받는다.
HttpStatus와 함께 선택적으로 reason과 cause를 추가 가능
언체크 예외을 상속받고 있어, 명시적으로 에러를 처리해주지 않아도 됨
ResponseStatusExceptionResolver 클래스가 ResponseStatusException 또는 @ResponseStatus 이 붙은 예외를 찾아서 처리
기본적인 예외 처리를 빠르게 적용할 수 있으므로, 손쉽게 프로토타이핑 가능
HttpStatus를 직접 설정하여, 예외 클래스와의 결합도 ↓
불필요하게 많은 별도의 예외 클래스를 만들지 않아도 됨
프로그래밍 방식으로 예외를 직접 생성하므로, 예외를 더욱 잘 제어 O
(비슷한 유형의 예외를 별도로 처리 가능. 응답마다 다른 상태 코드를 세팅 가능)
직접 예외 처리를 프로그래밍하므로, 일관된 예외 처리 X
예외 처리 코드의 중복
Spring 내부의 예외를 처리 어려움
예외가 WAS까지 전달되고, WAS의 에러 요청 전달이 진행됨
이런 한계점으로 인해,
API 에러 처리를 위해서는 @ExceptionHandler를 사용하는 방식이 多 사용됨
@RestController
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
@GetMapping("/product/{id}")
public Response getProduct(@PathVariable String id){
return productService.getProduct(id);
}
// 에러 처리
@ExceptionHandler(NoSuchElementFoundException.class)
public ResponseEntity<String> handleNoSuchElementFoundException(NoSuchElementFoundException exception) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(exception.getMessage());
}
}
매우 유연하게 에러를 처리할 수 있는 방법
@ExceptionHandler에 의해 발생한 예외는 ExceptionHandlerExceptionResolver에 의해 처리됨
Exception 클래스들을 속성으로 받아 처리할 예외를 지정 가능
ExceptionHandler 어노테이션에 예외 클래스를 지정하지 않는다면, 파라미터에 설정된 에러 클래스를 처리
@ResponseStatus와도 결합 가능
주의!
'@ExceptionHandler 에 등록된 예외 클래스'와 '파라미터로 받는 예외 클래스'가 동일해야 한다는 것이다.
만약 값이 다르다면, Spring 은 컴파일 시점에 에러를 내지 않다가 런타임 시점에 에러를 발생시킨다.
컨트롤러의 메소드
@ControllerAdvice 나 @RestControllerAdvice 가 있는 클래스의 메소드
@ResponseStatus와 달리, 에러 응답(payload)을 자유롭게 다룰 수 있다는 점에서 유연하다.
ExceptionHandler 의 파라미터로 HttpServletRequest나 WebRequest 등...을 얻을 수 있으며
반환 타입으로는 ResponseEntity, String, void 등... 자유롭게 활용 가능
@ExceptionHandler 에 등록된 예외 클래스와 파라미터로 받는 예와 클래스가 동일해야함
만약 값이 다르다면, Spring 은 컴파일 시점에 에러를 내지 않다가 런타임 시점에 에러를 발생시킨다.
@ExceptionHandler는 컨트롤러에 구현하므로, 특정 컨트롤러에서만 발생하는 예외만 처리된다.
→ 컨트롤러에 에러 처리 코드가 섞이면서, 에러 처리 코드가 중복될 가능성↑
→ 그래서 Spring 은 전역적으로 예외를 처리할 수 있는 좋은 기술을 제공해줌
구현 방법에 대해서는 아래 링크를 참고하자.
참고: @RestControllerAdvice 예외 처리 구현하기
이 어노테이션을 사용하면, 전역적으로 @ExceptionHandler 를 적용할 수 있다
두 어노테이션의 차이는 @Controller 와 RestController 의 차이와 같다.
하나의 클래스로, 모든 컨트롤러에 대해 전역적으로 예외 처리 가능
직접 정의한 에러 응답을 일관성있게 클라이언트에게 내려줄 수 있음
별도의 try-catch문이 없어, 코드의 가독성↑
여러 ControllerAdvice가 있을 때 @Order 어노테이션으로 순서를 지정하지 않는다면, Spring은 ControllerAdvice를 임의의 순서로 처리할 수 있음
→ 일관된 예외 응답을 위해서는 이러한 점에 주의
한 프로젝트당 하나의 ControllerAdvice만 관리하는 것이 좋다.
여러 ControllerAdvice 가 필요하다면, basePackages나 annotations 등을 지정해야 함
직접 구현한 Exception 클래스들은 한 공간에서 관리