Springboot Exception Handling - 예외처리 방법

haaaalin·2023년 3월 27일
0

Spring Boot에서 예외처리하는 방법을 낱낱이 파헤쳐 보자
왜 예외처리를 따로 해주어야 할까?

예외처리를 따로 해주지 않을 경우

  • 클라이언트 입장에서 유용한 정보 제공하기 어려움
  • 운영환경에서의 구현이 노출되기 때문에 해커의 위협에서 벗어나기 어렵다.

Setting

ErrorCode

@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 타입으로 에러 발생 시 명시할 코드 들을 정의해 놓는다.

ErrorResponse

@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의 형식을 정의하는 파일이다.

GlobalExceptionHandler

@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

@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를 임의의 순서로 처리할 수 있다.

일관된 예외처리를 위해서라면?

  • 한 프로젝트당 하나의 ControllerAdvice만 관리하는 것이 좋다.
  • 만약 여러 ControllerAdvice가 필요하다면 basePackages나 annotations 등을 지정해야 한다.
  • 직접 구현한 Exception 클래스들은 한 공간에서 관리한다.

✏️ 참고

profile
한 걸음 한 걸음 쌓아가자😎

0개의 댓글