Spring Boot에서 예외처리하는 방법을 낱낱이 파헤쳐 보자
왜 예외처리를 따로 해주어야 할까?
@Getter
@AllArgsConstructor
public enum ErrorCode {
// Global
INTERNAL_SERVER_ERROR(500, "G001", "서버 오류"),
INPUT_INVALID_VALUE(400, "G002", "잘못된 입력"),
private final int status;
private final String code;
private final String message;
}
먼저 이렇게 enum 타입으로 에러 발생 시 명시할 코드 들을 정의해 놓는다.
@Builder
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class ErrorResponse {
private String code;
private String message;
private List<FieldError> errors;
private ErrorResponse(ErrorCode code, List<FieldError> fieldErrors) {
this.code = code.getErrorCode();
this.message = code.getMessage();
this.errors = fieldErrors;
}
private ErrorResponse(ErrorCode code) {
this.code = code.getErrorCode();
this.message = code.getMessage();
this.errors = new ArrayList<>();
}
public static ErrorResponse of(final ErrorCode code, final BindingResult bindingResult) {
return new ErrorResponse(code, FieldError.of(bindingResult));
}
public static ErrorResponse of(ErrorCode code) {
return new ErrorResponse(code);
}
}
controller에서 예외 발생 시 ErrorResponse를 포함한 응답을 클라이언트 측에 반환할텐데, 그 ErrorResponse의 형식을 정의하는 파일이다.
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(Exception.class)
protected ResponseEntity<ErrorResponse> handleException(Exception e) {
log.error(e.getMessage(), e);
ErrorResponse response = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR);
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler
protected ResponseEntity<ErrorResponse> handleRuntimeException(BusinessException e) {
final ErrorCode errorCode = e.getErrorCode();
final ErrorResponse response =
ErrorResponse.builder()
.message(errorCode.getMessage())
.code(errorCode.getErrorCode())
.build();
log.warn(e.getMessage());
return new ResponseEntity<>(response, errorCode.getStatus());
}
@ExceptionHandler
protected ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(
MethodArgumentNotValidException e) {
final ErrorResponse response =
ErrorResponse.of(ErrorCode.INPUT_INVALID_VALUE, e.getBindingResult());
log.warn(e.getMessage());
return new ResponseEntity<>(response, BAD_REQUEST);
}
private ResponseEntity<Object> handleExceptionInternal(
Exception e, ErrorCode errorCode, WebRequest request) {
log.error(e.getMessage(), e);
return handleExceptionInternal(e, errorCode, errorCode.getStatus(), request);
}
private ResponseEntity<Object> handleExceptionInternal(
Exception e, ErrorCode errorCode, HttpStatus status, WebRequest request) {
return super.handleExceptionInternal(
e, ErrorResponse.of(errorCode), HttpHeaders.EMPTY, status, request);
}
}
@RestControllerAdvice
어노테이션을 사용한 GlobalExceptionHandler이다.
@ExceptionHandler
: @ExceptionHandler(xxException.class) : 발생한 xxException에 대해서 처리하는 메소드를 작성한다.
ResponseEntityExceptionHandler
:
Spring은 스프링 예외를 미리 처리해둔 ResponseEntityExceptionHandler
를 추상 클래스로 제공하고 있다. ResponseEntityExceptionHandler에는 스프링 예외에 대한 ExceptionHandler가 모두 구현되어 있으므로 ControllerAdvice 클래스가 이를 상속받게 하면 된다.
❓ 만약 이
ResponsEntityExceptionHandler
를 상속받지 않는다면?
스프링 예외들은 DefaultHandlerExceptionResolver가 처리하게 되고, 이에 따라 예외 처리기가 달라지므로 클라이언트가 일관되지 못한 에러 응답을 받지 못한다.
ResponseEntityExceptionHandler
를 상속시키는 것이 좋다. 또한 이는 기본적으로 에러 메세지를 반환하지 않으므로, 스프링 예외에 대한 에러 응답을 보내려면handleExceptionInternal
를 오버라이딩 해야 한다.
직접 구현한 Exception 클래스들은 한 공간에서 관리한다.
@Service
public class UserService {
UserRepository repository;
PasswordEncoder passwordEncoder;
public void save(User user){
Optional<User> aleadyUser = repository.findByEmail(user.getEmail());
if( aleadyUser.isPresent()){
throw new EmailDuplicateException("email duplicated",ErrorCode.EMAIL_DUPLICATION);
}
user.setPassword(passwordEncoder.encode(user.getPassword()));
repository.save(user);
}
}
Service 로직에서 보면 EmailDuplicateException
예외를 던지는 코드가 보인다.
저런 식으로 Exception 클래스를 직접 구현해서 사용할 수 있다.
@Getter
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
위 코드는 RuntimeException을 상속받는 BusinessException
예외 클래스를 직접 구현한 것이다.
여러 ControllerAdvice가 있을 때 @Order
어노테이션으로 순서를 지정하지 않는다면 Spring은 ControllerAdvice를 임의의 순서로 처리할 수 있다.
일관된 예외처리를 위해서라면?