스프링에서 예외처리하는 방식에 대해 알아보자.
스프링의 예외처리 방식을 이해하기 위해 먼저 스프링의 전체적인 흐름을 이해해야 한다.
- 위 그림은 Spring이 요청에 대한 처리를 어떠한 흐름으로 진행하는 지에 대한 그림이다. 단순한 순서(흐름)로만 보자.
(Interceptor가 Controller에게 요청을 위임하는 것은 아니다.)- 그림 속 과정에 대한 자세한 소개는 이 블로그에서 확인할 수 있다.
- 클라이언트의 요청을 디스패처 서블릿이 받음
- 요청 정보를 통해 요청을 위임할 컨트롤러를 찾음
- 요청을 컨트롤러로 위임할 핸들러 어댑터를 찾아서 전달함
- 핸들러 어댑터가 컨트롤러로 요청을 위임함
- 비지니스 로직을 처리함
- 컨트롤러가 반환값을 반환함
- HandlerAdapter가 반환값을 처리함
- 서버의 응답을 클라이언트로 반환함
BasicErrorController란 스프링 부트의 기본 예외처리 Controller이다.
때문에, 별다른 설정을 하지 않았다면 예외가 발생했을 때 BasicErrorController로 예외처리 요청이 전달된다.
예외 발생 시 요청 전달 흐름이다.
WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러
-> 컨트롤러(예외발생) -> 인터셉터 -> 서블릿 -> 필터 -> WAS
-> WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(BasicErrorController)
컨트롤러와 필터, 인터셉터가 두 번씩 호출되는 것을 알 수 있다. 또한 기본 에러는 클라이언트에게 status code 500에 "Internel server error"로만 응답하기 때문에, 클라이언트는 무슨 에러인지 알 수 없다.
이러한 바람직하지 못한 에러 처리를 위해 우리는 별도의 예외처리 전략을 사용해야 한다.
에러 처리를 메인 로직으로부터 분리함
대부분의 HandlerExceptionResolver는 발생한 Exception을 catch하고 HTTP 상태나 응답 메세지 등을 설정한다.
WAS는 이를 정상적인 응답으로 인식하여, 위에서 설명한 복잡한 WAS의 에러 전달이 진행되지 않는 것.
적합한 예외 처리를 위해 HandlerExceptionResolver 구현체들을 빈으로 등록해서 관리한다. 아래는 빈으로 등록된 4가지 구현체들이다. (우선순위 순)
DefaultErrorAttributes
ExceptionHandlerExceptionResolver
ResponseStatusExceptionResolver
DefaultHandlerExceptionResolver
Spring에서 ExceptionResolver를 동작시켜 에러를 처리할 때 사용하는 도구들을 살펴보자.
@RestControllerAdvice
(또는 @ControllerAdvice
) 애노테이션을 사용해야 한다.@ExceptionHandler
를 전역적으로 적용할 수 있게 해준다.@ControllerAdvice
와의 차이점은 에러 응답을 JSON
으로 내려준다는 것이다.그럼 이제 @ExceptionHandler와 @RestControllerAdvice를 사용해 예외 처리를 하는 간단한 실습을 진행해보자.
- 에러 별, Http Status code와 에러 메시지를 정의하는 파일이다.
- INVALID_INPUT_VALUE는 Validation 예외 때 이용할 상수
// hello.springmvc.first.Exceptions.ErrorCode.java
@Getter
@RequiredArgsConstructor
public enum ErrorCode {
INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "INVALID INPUT VALUE");
private final HttpStatus status;
private final String message;
}
- 클라이언트에게 전달할 에러의 응답 형태를 정의하는 클래스다.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ErrorResponse {
private String message;
private int status;
private List<FieldError> errors;
private ErrorResponse(final ErrorCode code) {
this.message = code.getMessage();
this.status = code.getStatus().value();
}
private ErrorResponse(final ErrorCode code, final List<FieldError> errors) {
this.message = code.getMessage();
this.status = code.getStatus().value();
this.errors = errors;
}
public static ErrorResponse of(final ErrorCode code) {
return new ErrorResponse(code);
}
public static ErrorResponse of(final ErrorCode code, final BindingResult bindingResult) {
return new ErrorResponse(code, FieldError.of(bindingResult));
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public static class FieldError {
private String field;
private String value;
private FieldError(final String field, final String value) {
this.field = field;
this.value = value;
}
private static List<FieldError> of(final BindingResult bindingResult) {
final List<org.springframework.validation.FieldError> fieldErrors = bindingResult.getFieldErrors();
return fieldErrors.stream()
.map(error -> new FieldError(
error.getField(),
error.getRejectedValue() == null ? "" : error.getRejectedValue().toString()))
.collect(Collectors.toList());
}
}
}
- 앞에서 언급한
@RestControllerAdvice
,@ExceptionHandler
를 사용해 전역 예외 처리를 담당하는 Class이다.MethodArgumentNotValidException
: 데이터의 유효성 검사(@Valid
)가 실패한 경우 throw되는 Exception이다.
- 위 에러가 발생했을 때 이 메서드가 에러 처리를 위임한다.
@RestControllerAdvice
public class GlobalExceptionHandler {
// @Valid 예외 발생
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
final var response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult());
return new ResponseEntity<>(response, ErrorCode.INVALID_INPUT_VALUE.getStatus());
}
}
https://mangkyu.tistory.com/18
https://mangkyu.tistory.com/173
https://mangkyu.tistory.com/204