기본적인 에러 처리 방식은 에러 컨트롤러를 한 번 더 호출하는 것이다.
그러므로 필터나 인터셉터가 다시 호출될 수 있는데, 이를 제어하기 위해서는 별도의 설정이 필요하다.
웹서버와 웹 컨테이너의 결합
(웹 컨테이너 : 클라이언트의 요청이 있을 때, 내부의 프로그램을 통해 결과를 만들어내고, 이것을 다시 클라이언트에 전달)
다양한 기능을 컨테이너에 구현하여 다양한 역할을 수행할 수 있는 서버
동적인 데이터를 처리하는 서버
(DB와 연결되어 데이터를 주고 받기 or 프로그램으로 데이터 조작이 필요한 경우)
tomcat 을 흔히 WAS(Web Application Server)라고 말한다.
서블릿 기술이므로, 필터 등록(FilterRegistrationBean) 시에 호출될 dispatcherType 타입을 설정 가능
별도의 설정이 없다면, REQUEST일 경우에만 필터가 호출
Spring 기술이므로, dispatcherType을 설정할 수 없어 URI 패턴으로 처리가 필요
그러나, Spring Boot 에서는 WAS까지 직접 제어하게 되면서,
→ 그러므로 클라이언트는 에러 처리 작업이 진행되었는지 알 수 X
Spring 은 처음부터 에러 처리를 위한 BasicErrorController 를 구현해두었다.
별도의 설정이 없다면, /error로 에러 요청을 다시 전달하는 WAS의 설정에 의해 BasicErrorController 로 에러처리 요청이 전달된다.
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
private final ErrorProperties errorProperties;
...
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
...
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
...
return new ResponseEntity<>(body, status);
}
...
}
이러한 기본 설정으로는 다음과 같은 에러 응답을 받게 된다.
이는 클라이언트 입장에서는 어떤 에러가 발생했는지 명확히 파악이 어려울 수 있다.
{
"timestamp": "2021-12-31T03:35:44.675+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/product/5000"
}
properties를 통해 에러 응답을 작성해준다고 해도, status는 여전히 500이며 유의미한 에러 응답을 전달하지 못한다.
{
"timestamp": "2021-12-31T03:35:44.675+00:00",
"status": 500,
"error": "Internal Server Error",
"trace": "java.util.NoSuchElementException: No value present ...",
"message": "No value present",
"path": "/product/5000"
}
ExceptionHandlerExceptionResolver: 에러 응답을 위한 Controller나 ControllerAdvice에 있는 ExceptionHandler를 처리함
ResponseStatusExceptionResolver: Http 상태 코드를 지정하는 @ResponseStatus 또는 ResponseStatusException를 처리함
DefaultHandlerExceptionResolver: 스프링 내부의 기본 예외들을 처리한다.
각 예외 처리기들은 스프링의 빈으로 등록되어 있고,
예외가 발생하면 순차적으로 다음의 Resolver들이 처리가능한지 판별한 후에 예외가 처리된다.
(2. Spring 의 예외 처리 中 ② Object handler 참고)
1 ~ 6 : ExceptionHandlerExceptionResolver 가 동작
7 ~ 9 : ResponseStatusExceptionResolver 가 동작
10 ~ 12 : DefaultHandlerExceptionResolver가 동작
12 : 적합한 ExceptionResolver 가 없음
Spring 은 예외가 발생하면
가장 구체적인 예외 핸들러를 먼저 찾고, 없으면 부모 예외의 핸들러를 찾는다.
@RestController
@RequiredArgsConstructor
public class ProductController {
...
@ExceptionHandler(NoSuchElementFoundException.class)
public ResponseEntity<ErrorResponse> handleItemNotFoundException(NoSuchElementFoundException exception) {
...
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
...
}
// 부모 예외 핸들러
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAllUncaughtException(Exception exception) {
...
}
}
NullPointerException 이 발생했다고 가정해보자.
위에서는 NullPointerException 처리기가 없으므로, Exception 에 대한 처리기가 찾아진다.
DB 에 접근하기 위한 @Repository 빈들에는 대표적으로 예외 전환 기법이 사용되고 있다.
(H2 DB 에서 발생한 에러가 최종적으로 Spring 에러로 추상화)
추상화 X
Spring 어플리케이션 이용 시, 어떤 DB가 사용될지 모른다.
그런데 만약
DB 를 MySQL에서 PostgreSQL로 전환하는 경우
에러 추상화가 없다면, 모든 MySQL 에러를 PostgreSQL로 전환해야한다.
추상화 O
Spring 의 @Repository 에는 각기 다른 DB 에 의한 에러를 DataAccessException(Spring 의 DB 에러)로 전환해주는 기능이 있다.
→ 이를 통해, DB 에 종속되지 않는 개발이 가능
DB 에 의한 에러는 일반적으로 체크 예외이지만,
DB 에 의한 체크 예외를 throws 받아도, 특별히 할 작업은 따로 없고 에러 코드를 내려주는 것 뿐이다.
예시
중복된 ID로 가입을 하는 경우, Constraint 에러가 발생하였다면
우리는 올바른 에러 코드를 내려주면 된다 O
체크 예외로 처리할 필요 X → 만약 체크 예외를 그대로 가져간다면, 불필요하게 throw해주어야 하는 코드만 多
Spring 은 이런 문제를 해결하기 위해 예외를 언체크 예외로 정의
→ 이를 통해, 무분별한 throw를 하지 않아도 된다.