about.Programing/ToyProject

[EasyValet. Refactorying] # 11. GlobalException에 @ExceptionHandler가 너무 많은데?

Logan. 2022. 10. 12. 12:35

Exception을 한곳에서 처리하는 객체의 마지막 라인

프로젝트 코드를 작성하다가 새로운 예외가 필요했습니다. 예외 객체를 만들고 전역에서 발생하는 Exception을 처리하는 클래스에 예외를 추가하려하는데 스크롤을 한도 끝도 없이 내려야했습니다. 마지막 라인을 보는데 212라인? 이게 말이됩니까? '서비스가 커지고 예외가 더 많아지면 어떻게 처리하고 코드 가독성을 어떻게 할꺼며 와... 답이 없다.' 정말 답이 없었습니다.

 

리펙토링하면 분명 줄일 수 있는 코드였습니다. 우선 제가 가진 선택권은 두가지가 있었습니다. 

  1. 기존에 사용하던 RuntimeException을 모두 확장하고 있으니 RuntimeException을 처리하는 ExceptionHandler을 만드는 작업을 진행할 것인가?
  2. 트리형식의 계층 구조를 만들고 조금 더 깔끔하게 코드를 만들 것인가? 

 

조금 더 깔끔한 코드를 가질 수 있을 것 같아서 두번째 방식을 사용하기로했습니다. 설계는 아래와 같습니다. 

  1. ErrorCode(Enum)를 만듭니다. 이 코드는 status와 도메인 범주를 의미하는 코드, 메세지 세가지를 추가로 가지고있습니다. 
  2. ApiResponse를 새로하나 만듭니다. -- 기존에는 실패와 성공 response가 따로 있었습니다. 이를 합치기 위함입니다.
  3. RuntimeException을 확장한 최상위 커스텀 클래스를 만듭니다. 그리고 이 객체는 ErrorCode를 가지고 있습니다.  
  4. 이제 최상위 커스텀 클래스를 확장한 커스텀 클래스들을 만들겁니다.( 범주를 발생상황별로 두려고합니다. InvalidArgument,InvalidToken)
  5. 최상위 커스텀 클래스를 받는 @ExceptionHandler만 남깁니다.

 

 

이게 제 예상 시나리오고 이제 리펙토링을 시작해보겠습니다.

 

리펙토링이니깐 기능에 영향을 주지 않고 코드만 변경되어야합니다.(코드가 추가되는 것은 상관 없을 것 같습니다.)

 

#1. 리펙토링

@Getter
public enum ErrorCode {

    // Common
    INVALID_ARGUMENT(400, "C001", "잘못된 입력값입니다."),
    METHOD_NOT_ALLOWED(405, "C002", "기능 사용 권한이 없습니다."),
    HANDLE_ACCESS_DENIED(403, "C006", "권한이 없습니다."),

    // Member
    EMAIL_DUPLICATION(400, "M001", "중복된 이메일이 존재합니다."),
    LOGIN_INPUT_INVALID(400, "M002", "로그인 정보가 없습니다.")

    ;
    private final int status;
    private final String code;
    private final String message;


    ErrorCode(final int status, final String code, final String message) {
        this.status = status;
        this.message = message;
        this.code = code;
    }
}

우선 errorCode가 모두 작성되어 있는 enum을 만들었습니다.이제 enum을 통하면 에러 메세지를 타이핑할 필요가 없습니다. 추가로 필요하다면 enum에 추가하고 가져다 쓰면됩니다. 1번 ✅ 

 

이제 성공이던 에러던 하나로 처리할 수 있는 객체를 만들어 보겠습니다. 

@ToString
@Getter
@EqualsAndHashCode
public class ApiResponse<T> {

  @DateTimeFormat(pattern = "yyyy-MM-dd 'T'HH:mm")
  private final String localDateTime;
  private final boolean success;
  private final T data;

  public ApiResponse(boolean success, T data) {
    localDateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
    this.success = success;
    this.data = data;
  }

  public static <T> ApiResponse success(T data) {
    return new ApiResponse(true, data);
  }

  public static ApiResponse fail(ErrorCode errorCode) {
    return new ApiResponse(false, new ErrorBody(errorCode.getCode(), errorCode.getStatus(),
        errorCode.getMessage()));
  }
  public static ApiResponse fail(ErrorCode errorCode, String message) {
    return new ApiResponse(false, new ErrorBody(errorCode.getCode(), errorCode.getStatus(),
        message));
  }
  
  @ToString
  @EqualsAndHashCode
  @Getter
  @AllArgsConstructor
  public static final class ErrorBody {

    private final String code;
    private final int status;
    private final String message;
  }
}

이제 Errorcod를 통해서 어떤 객체도 반환할 수 있습니다. Errorcode를 가지고 있는 커스텀 예외 중 최상위 객체를 만들어보겠습니다. 목적은 다형성을 이용해서 최상위 객체로 모든 Exception을 하나의 메서드에서 처리하려고 합니다.  2 ✅ 

 

/**
 * @Explain System전체에서 사용할 최상위 커스텀 예외
 * @Howto 모든 커스텀 예외는 이 예외를 확장해서 사용할 것.
 * @Field ErrorCode를 사용함으로 에러 코드로 모든 설명을 하게됨.
 **/
public class BaseException extends RuntimeException {

  private ErrorCode errorCode;

  public BaseException(String message, ErrorCode errorCode) {
    super(message);
    this.errorCode = errorCode;
  }

  public BaseException(ErrorCode errorCode) {
    super(errorCode.getMessage());
    this.errorCode = errorCode;
  }
  public BaseException(){}
  public ErrorCode getErrorCode() {
    return errorCode;
  }
}

ErrorCode를 받아서 가지고 있는 최상위 커스텀 객체를 만들었습니다. 3 ✅ 

 

이제 ControllerAdvice가 붙어있는 GlobalExceptionHandler에 BaseException을 처리하는 ExceptionHandler를 추가해보겠습니다.

 

@ExceptionHandler(BaseException.class)
protected ApiResponse handleBaseException(BaseException e) {
  e.getClass().toString();
  log.info("GlobalExceptionHandler :: {} = {} ", e.getClass().getName(), e.getMessage());
   return ApiResponse.fail(e.getErrorCode());
}

전달받은 에러코드에 내용들을 예외 응답객체로 만들어서 반환해줄 수 있게되었습니다. 

BaseException을 확장한 클래스를 하나 만들고 그 ErrorCode.INVALID_ARGUMENT를 던져주면 아래와 같이 반환됩니다. 4 ✅ 

 

정리

큰 범위의 예외객체를 만들고 상세 예외 설명 메세지는 ErrorCode를 통해 관리할 수 있습니다. 예외객체를 만들 때 적용될 범위는 도메인을 기준으로 만들 수도 있고 도메인을 그 이외의 시스템적인 부분들을 가지고 만들 수도 있습니다. 또한 예외객체를 추가로 생성하지 않아도 충분히 예외처리가 가능합니다. 프로젝트마다 정해서 BaseException을 만들고 ErrorCode를 사용해서 언제든 전역 예외처리를 할 수 있게 되었습니다. 

 

프로젝트에 이미 적용되어 있는 부분도 모두 정리해야겠지만 큰 틀과 사용할 수 있는 예외가 생겼음으로 차츰 차츰해 나가면 될 것 같습니다. 진짜 코드가 훨씬 깔끔하고 짧아졌습니다.