API 문서에서 에러를 전달할 때 일괄적인 방식으로 에러 메시지를 내려줄 필요가 있다고 느꼈습니다.
예를 들어 회원 가입을 할 때 409 에러가 뜬다면 로그인 아이디에서 나는 문제인지 이메일에서 나는 문제인지 에러 코드로 바로 확인할 수 있도록 공통 error code를 만들어서 이를 적용하게 되었습니다.
HTTP Status의 에러 응답 부분을 더 세분화하여 각 상황별 에러 코드를 정리한 에러 목록입니다.
Error Code는 반드시 필요한 건 아니지만 사용하는 서비스의 경우, 서비스 별로 Error Code를 정의해서 사용합니다.
상황에 따른 에러를 세분화해 전달할 수 있어 API 문서 가독성이 높아집니다.
400 Bad Request
, 403 Forbidden
네이버와 카카오의 error code 레퍼런스를 보면서 먼저 에러 코드 형태를 정리하고, 상황별 에러 코드들을 정리했습니다.
{
"status" : 409,
"code" : "E409001",
"message" : "LOGINID_CONFLICT"
}
@Getter
public enum ErrorCode {
...
E409001("LOGINID_CONFLICT"),
E409002("EMAIL_CONFLICT"),
...
;
private String errorMessage;
ErrorCode(String errorMessage) {
this.errorMessage = errorMessage;
}
}
@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;
}
}
@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;
}
}
@Getter
public class ApiException extends RuntimeException {
private ErrorType errorType;
public ApiException(ErrorType errorType) {
this.errorType = errorType;
}
}
@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 문서 가독성도 높아지는 경험이었습니다.