| 1 | package com.project.auth.presentation.support.exception; | |
| 2 | ||
| 3 | import com.project.auth.application.support.exception.AuthErrorCode; | |
| 4 | import com.project.auth.application.support.exception.ClientFacingErrorCode; | |
| 5 | import com.project.auth.application.support.exception.CommonErrorCode; | |
| 6 | import org.slf4j.Logger; | |
| 7 | import org.slf4j.LoggerFactory; | |
| 8 | import org.springframework.http.HttpStatus; | |
| 9 | ||
| 10 | public final class ApiErrorHttpStatusMapper { | |
| 11 | ||
| 12 | private static final Logger log = LoggerFactory.getLogger(ApiErrorHttpStatusMapper.class); | |
| 13 | ||
| 14 | private ApiErrorHttpStatusMapper() { | |
| 15 | } | |
| 16 | ||
| 17 | /** | |
| 18 | * default 분기가 존재하는 이유: {@link ClientFacingErrorCode}가 non-sealed이므로 | |
| 19 | * (application/presentation 외 모듈도 레이어 경계를 넘지 않고 자체 구현을 추가할 수 있다) | |
| 20 | * 컴파일러가 exhaustiveness를 강제할 수 없다. 새로운 구현이 매핑 테이블에 누락된 채 | |
| 21 | * 들어오면 조용히 500이 되는 대신 WARN 로그로 드러내어 운영 대시보드에서 식별 가능하게 한다. | |
| 22 | * | |
| 23 | * "새 ClientFacingErrorCode 구현은 반드시 매핑 테이블에 등록해야 한다"는 아키텍처 차원의 | |
| 24 | * 게이트는 ApiErrorHttpStatusMapperClientFacingCoverageTest에 있다. | |
| 25 | */ | |
| 26 | public static HttpStatus map(ClientFacingErrorCode errorCode) { | |
| 27 |
1
1. map : replaced return value with null for com/project/auth/presentation/support/exception/ApiErrorHttpStatusMapper::map → KILLED |
return switch (errorCode) { |
| 28 | case CommonErrorCode commonErrorCode -> mapCommon(commonErrorCode); | |
| 29 | case AuthErrorCode authErrorCode -> mapAuth(authErrorCode); | |
| 30 | case PresentationErrorCode presentationErrorCode -> mapPresentation(presentationErrorCode); | |
| 31 | default -> { | |
| 32 | log.warn("Unmapped ClientFacingErrorCode reached default branch. type={} code={} -> falling back to 500", | |
| 33 | errorCode.getClass().getName(), errorCode.code()); | |
| 34 | yield HttpStatus.INTERNAL_SERVER_ERROR; | |
| 35 | } | |
| 36 | }; | |
| 37 | } | |
| 38 | ||
| 39 | private static HttpStatus mapCommon(CommonErrorCode errorCode) { | |
| 40 |
1
1. mapCommon : replaced return value with null for com/project/auth/presentation/support/exception/ApiErrorHttpStatusMapper::mapCommon → KILLED |
return switch (errorCode) { |
| 41 | case INTERNAL_SERVER_ERROR -> HttpStatus.INTERNAL_SERVER_ERROR; | |
| 42 | }; | |
| 43 | } | |
| 44 | ||
| 45 | private static HttpStatus mapPresentation(PresentationErrorCode errorCode) { | |
| 46 |
1
1. mapPresentation : replaced return value with null for com/project/auth/presentation/support/exception/ApiErrorHttpStatusMapper::mapPresentation → KILLED |
return switch (errorCode) { |
| 47 | case INVALID_INPUT, INVALID_REQUEST_BODY, MISSING_PARAMETER, CONSTRAINT_VIOLATION, MISSING_HEADER, REQUEST_BINDING_FAILED, TYPE_MISMATCH, INVALID_PARAMETER -> HttpStatus.BAD_REQUEST; | |
| 48 | case METHOD_NOT_ALLOWED -> HttpStatus.METHOD_NOT_ALLOWED; | |
| 49 | case RESOURCE_NOT_FOUND -> HttpStatus.NOT_FOUND; | |
| 50 | case UNSUPPORTED_MEDIA_TYPE -> HttpStatus.UNSUPPORTED_MEDIA_TYPE; | |
| 51 | case NOT_ACCEPTABLE -> HttpStatus.NOT_ACCEPTABLE; | |
| 52 | case PAYLOAD_TOO_LARGE -> HttpStatus.CONTENT_TOO_LARGE; | |
| 53 | case MESSAGE_NOT_WRITABLE -> HttpStatus.INTERNAL_SERVER_ERROR; | |
| 54 | case UNHANDLED_CLIENT_ERROR -> HttpStatus.BAD_REQUEST; | |
| 55 | }; | |
| 56 | } | |
| 57 | ||
| 58 | private static HttpStatus mapAuth(AuthErrorCode errorCode) { | |
| 59 |
1
1. mapAuth : replaced return value with null for com/project/auth/presentation/support/exception/ApiErrorHttpStatusMapper::mapAuth → KILLED |
return switch (errorCode) { |
| 60 | case AUTHENTICATION_REQUIRED -> HttpStatus.UNAUTHORIZED; | |
| 61 | case ACCESS_DENIED -> HttpStatus.FORBIDDEN; | |
| 62 | case KEYCLOAK_CLAIMS_INVALID -> HttpStatus.BAD_REQUEST; | |
| 63 | case KEYCLOAK_USER_NOT_FOUND -> HttpStatus.NOT_FOUND; | |
| 64 | }; | |
| 65 | } | |
| 66 | } | |
Mutations | ||
| 27 |
1.1 |
|
| 40 |
1.1 |
|
| 46 |
1.1 |
|
| 59 |
1.1 |