Error Code 적용기

ollie·2023년 12월 8일
2

배경 🐈

API 문서에서 에러를 전달할 때 일괄적인 방식으로 에러 메시지를 내려줄 필요가 있다고 느꼈습니다.
예를 들어 회원 가입을 할 때 409 에러가 뜬다면 로그인 아이디에서 나는 문제인지 이메일에서 나는 문제인지 에러 코드로 바로 확인할 수 있도록 공통 error code를 만들어서 이를 적용하게 되었습니다.

📍 Error Code란?

HTTP Status의 에러 응답 부분을 더 세분화하여 각 상황별 에러 코드를 정리한 에러 목록입니다.
Error Code는 반드시 필요한 건 아니지만 사용하는 서비스의 경우, 서비스 별로 Error Code를 정의해서 사용합니다.
상황에 따른 에러를 세분화해 전달할 수 있어 API 문서 가독성이 높아집니다.

HTTP Status Code

  • HTTP 요청에 대한 응답 코드
  • HTTP Status Code 중 400 번대와 500 번대가 에러 응답을 담당합니다. ex ) 400 Bad Request, 403 Forbidden

📍 레퍼런스 참고하여 구상하기

네이버카카오의 error code 레퍼런스를 보면서 먼저 에러 코드 형태를 정리하고, 상황별 에러 코드들을 정리했습니다.

에러 반환 형태

{
    "status" : 409,
	"code" : "E409001",
	"message" : "LOGINID_CONFLICT"
}

공통 에러 처리 모델링

공통에러 처리 모델링

상황별 에러 코드 정리

공통에러처리코드 정리


📍 코드에 적용하기

ErrorCode

@Getter
public enum ErrorCode {
    ...
    E409001("LOGINID_CONFLICT"),
    E409002("EMAIL_CONFLICT"),
    ...
		;

    private String errorMessage;

    ErrorCode(String errorMessage) {
        this.errorMessage = errorMessage;
    }
}

ErrorType

@Getter
public enum ErrorType {
    ...
    LOGINID_CONFLICT(
            HttpStatus.CONFLICT,
            ErrorCode.E409001,
            ErrorCode.E409001.getErrorMessage()
    ),
    EMAIL_CONFLICT(
            HttpStatus.CONFLICT,
            ErrorCode.E409002,
            ErrorCode.E409002.getErrorMessage()
    ),
		...
    ;

    private final HttpStatus status;
    private final ErrorCode code;
    private final String message;

    ErrorType(HttpStatus status, ErrorCode code, String message) {
        this.status = status;
        this.code = code;
        this.message = message;
    }
}

ErrorResponse

@Getter
public class ErrorResponse {
    private int status;
    private String code;
    private String message;
    HashMap<String, Object> response = new LinkedHashMap<>();

    public HashMap<String, Object> updateErrorResponse(ErrorType errorType) {
        this.status = errorType.getStatus().value();
        this.code = errorType.getCode().toString();
        this.message = errorType.getMessage();

        response.put("status", status);
        response.put("code", code);
        response.put("message", message);
        return response;
    }
}

ApiException

@Getter
public class ApiException extends RuntimeException {
    private ErrorType errorType;

    public ApiException(ErrorType errorType) {
        this.errorType = errorType;
    }
}

CustomExceptionHandler

@Slf4j
@RestControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {
    ErrorResponse response = new ErrorResponse();

    @ExceptionHandler(ApiException.class)
    public ResponseEntity handleCommonApiException(ApiException ex) {
        log.error("ApiException: {}", ex);
        return ResponseEntity.status(ex.getErrorType().getStatus()).body(response.updateErrorResponse(ex.getErrorType()));
    }
		...
}

📍 고민한 점

에러를 어디까지 정의하는 게 맞을까? 💭

세부적인 에러 상황을 정의하기 위해 사용하는 것이지만 각 상황에 대해 너무 세부적으로 나눠 놓으면 오히려 가독성이 떨어질 것 같아 어디까지 정의하는 것이 맞는지에 대한 고민을 하였습니다.

accessToken 갱신 시 refreshToken 문제로 발생하는 여러 경우의 에러를 INVALID_REFRESHTOKEN으로 통합하였습니다.

저장해둔 refreshToken와 다르거나 refreshToken 만료일이 지났거나 등 다양한 경우로 에러가 발생하는데, 프론트엔드의 입장에선 어떤 문제인지 세부적으로 아는 것보다 유효하지 않은 refreshToken이라는 것을 인지해 다시 로그인을 통해 유효한 refreshToken을 새로 얻어야 한다는 것만 알면 될 것이라 생각하였습니다.

인증 절차를 거치지 않아 발생하는 에러에 대해 UNAUTHENTICATED_STATUS 으로 통합하였습니다.

이메일 인증을 하지 않은 상태에서 회원 가입 시 발생하는 에러를 따로 분리하여 정의할까 고민하였습니다. 이메일 인증 에러는 회원가입 시에만 필요한 에러라 따로 정의해두는건 과한 정의하는 것이라고 느껴졌습니다. 그래서 로그인을 하지 않은 상황이나 이메일 인증을 하지 않은 상황 등 인증을 거치지 않는 상황에서의 요청 시, UNAUTHENTICATED_STATUS 로 에러를 반환하도록 정리하였습니다.


📍 느낀점

공통 에러 처리 시 제일 중요한 부분은 상황별 에러 정의를 깔끔하게 해두고, 이를 문서화하여 프론트엔드가 볼 수 있도록 공유하는 것이라고 생각합니다. 상황에 따라 매번 다른 에러 메시지보다 특정 상황에서 어떤 에러 메시지를 보낼건지 미리 정해서 더 일괄적인 에러 응답을 할 수 있었고, API 문서 가독성도 높아지는 경험이었습니다.

profile
생각하는 개발자가 되겠습니다 💡

0개의 댓글