ApiErrorHttpStatusMapper.java
package com.project.auth.presentation.support.exception;
import com.project.auth.application.support.exception.AuthErrorCode;
import com.project.auth.application.support.exception.ClientFacingErrorCode;
import com.project.auth.application.support.exception.CommonErrorCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
public final class ApiErrorHttpStatusMapper {
private static final Logger log = LoggerFactory.getLogger(ApiErrorHttpStatusMapper.class);
private ApiErrorHttpStatusMapper() {
}
/**
* default 분기가 존재하는 이유: {@link ClientFacingErrorCode}가 non-sealed이므로
* (application/presentation 외 모듈도 레이어 경계를 넘지 않고 자체 구현을 추가할 수 있다)
* 컴파일러가 exhaustiveness를 강제할 수 없다. 새로운 구현이 매핑 테이블에 누락된 채
* 들어오면 조용히 500이 되는 대신 WARN 로그로 드러내어 운영 대시보드에서 식별 가능하게 한다.
*
* "새 ClientFacingErrorCode 구현은 반드시 매핑 테이블에 등록해야 한다"는 아키텍처 차원의
* 게이트는 ApiErrorHttpStatusMapperClientFacingCoverageTest에 있다.
*/
public static HttpStatus map(ClientFacingErrorCode errorCode) {
return switch (errorCode) {
case CommonErrorCode commonErrorCode -> mapCommon(commonErrorCode);
case AuthErrorCode authErrorCode -> mapAuth(authErrorCode);
case PresentationErrorCode presentationErrorCode -> mapPresentation(presentationErrorCode);
default -> {
log.warn("Unmapped ClientFacingErrorCode reached default branch. type={} code={} -> falling back to 500",
errorCode.getClass().getName(), errorCode.code());
yield HttpStatus.INTERNAL_SERVER_ERROR;
}
};
}
private static HttpStatus mapCommon(CommonErrorCode errorCode) {
return switch (errorCode) {
case INTERNAL_SERVER_ERROR -> HttpStatus.INTERNAL_SERVER_ERROR;
};
}
private static HttpStatus mapPresentation(PresentationErrorCode errorCode) {
return switch (errorCode) {
case INVALID_INPUT, INVALID_REQUEST_BODY, MISSING_PARAMETER, CONSTRAINT_VIOLATION, MISSING_HEADER, REQUEST_BINDING_FAILED, TYPE_MISMATCH, INVALID_PARAMETER -> HttpStatus.BAD_REQUEST;
case METHOD_NOT_ALLOWED -> HttpStatus.METHOD_NOT_ALLOWED;
case RESOURCE_NOT_FOUND -> HttpStatus.NOT_FOUND;
case UNSUPPORTED_MEDIA_TYPE -> HttpStatus.UNSUPPORTED_MEDIA_TYPE;
case NOT_ACCEPTABLE -> HttpStatus.NOT_ACCEPTABLE;
case PAYLOAD_TOO_LARGE -> HttpStatus.CONTENT_TOO_LARGE;
case MESSAGE_NOT_WRITABLE -> HttpStatus.INTERNAL_SERVER_ERROR;
case UNHANDLED_CLIENT_ERROR -> HttpStatus.BAD_REQUEST;
};
}
private static HttpStatus mapAuth(AuthErrorCode errorCode) {
return switch (errorCode) {
case AUTHENTICATION_REQUIRED -> HttpStatus.UNAUTHORIZED;
case ACCESS_DENIED -> HttpStatus.FORBIDDEN;
case KEYCLOAK_CLAIMS_INVALID -> HttpStatus.BAD_REQUEST;
case KEYCLOAK_USER_NOT_FOUND -> HttpStatus.NOT_FOUND;
};
}
}