16 KiB
title, source_type, status, confidence, tags, related_projects, last_reviewed
| title | source_type | status | confidence | tags | related_projects | last_reviewed | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ca-tmpl - API Error Envelope 결정 (custom envelope, ProblemDetail 거부) | project | verified | high |
|
|
2026-07-02 |
ca-tmpl - API Error Envelope 결정 (custom envelope, ProblemDetail 거부)
Layer:
wiki/projects/— 내 프로젝트 사실. 일반 개념은 wiki/concepts/api-error-envelope-design 참고.
프로젝트 컨텍스트
ca-tmpl은 Clean Architecture 기반 백엔드 skeleton 프로젝트다. 운영 환경에서 API 실패 응답을 일관된 구조로 직렬화하고, client가 분기/재시도/관측 가능하도록 만들기 위해 자체 error envelope을 설계했다. RFC 7807 ProblemDetail이 Spring 6+ 기본 지원이지만 의식적으로 거부하고 다음 shape을 채택했다.
{
success: boolean,
data: <T> | null,
error: {
code: string,
category: string,
message: string,
retryable: boolean,
details: <항목별 오류 배열> | null
} | null,
meta: { requestId, traceId, correlationId, ... }
}
진행 상태: Phase C2 (구현) 완료 (2026-06-01). envelope record, exception handler, error response factory가 코드에 존재하고 ./gradlew check (전 모듈 test + ArchUnit)가 통과한다. 단 Retry-After 헤더 발행과 span ERROR 기록은 seam/stub 상태이며 owner branch에 위임돼 있다(아래 명시).
Ground-truth 대조 (2026-06-04, ca-tmpl @0c996fc "운영 에러 관측성 foundation 계약 구현", HEAD
db61075에서도 존재 확인): 아래 envelope field shape·클래스·enum·ArchUnit/config는 ca-tmpl 코드 실측으로 일치 확인. 패키지 root는dev.caskeleton.*(이전 stale 추출의com.example.blog/sample-ticket류는 발견되지 않음 — 현재 sample 모듈은sample-portfolio). 모듈 경로는src/<module>/src/main/java/dev/caskeleton/....
실제 구현 내용 (actually-implemented)
/home/donghyeon/workspace/ca-tmpl 코드에 실재 (grep 확인):
shared-contract/response/Envelope.java—record Envelope<T>(boolean success, T data, ApiError error, ResponseMeta meta). success/failure가 한 shape 공유,ok()/failure()팩토리. RFC 7807 거부 javadoc 명시.shared-contract/response/ApiError.java—record ApiError(String code, String category, String message, boolean retryable, Object details).category가 1급 필드(10-enum 이름),retryable1급,details는 code별 polymorphic.shared-contract/response/ResponseMeta.java—record ResponseMeta(String requestId, String traceId, String correlationId, ...)— 평면traceId를 대체한 meta 객체(D20).shared-contract/error/Category.java— 10-value 운영 분류 enum.shared-contract/error/OperationalError.java+error/ApiErrorCode.java— code 카탈로그 +category()매핑(VALIDATION_FAILED/MAPPING_FAILED/… →VALIDATION,UNAUTHENTICATED/INVALID_TOKEN→AUTH,FORBIDDEN→AUTHZ,ROUTE_NOT_FOUND→NOT_FOUND,INTERNAL_ERROR→INTERNAL).retryable은 per-code 유지.adapter-web/error/GlobalExceptionHandler.java+error/ErrorResponseFactory.java— 예외 → envelope 변환,code.category().name()주입.adapter-web/envelope/EnvelopeBodyAdvice.java— 성공 응답 envelope 래핑.feature-api-contract-baseline이후 transport failure 매핑 — 413(PAYLOAD_TOO_LARGE), 406(NOT_ACCEPTABLE), 415(UNSUPPORTED_MEDIA_TYPE), 405(METHOD_NOT_ALLOWED+Allowheader), 412(PRECONDITION_FAILED) 를 같은 envelope shape으로 반환하되, category/status 의미는 보존한다. Spring MVCResponseEntityExceptionHandler가 이미 다루는 umbrella exception은@ExceptionHandler중복 등록이 아니라 protected override로 처리한다.
ProblemDetail 거부가 빌드 타임에 강제된다 (코드 실측):
app-bootstrap/.../architecture/CleanArchitectureTest.java(ArchUnit) —org.springframework.http.ProblemDetailimport 금지 규칙(L355 "D5: RFC 7807 ProblemDetail is explicitly rejected").app-bootstrap/src/main/resources/application.yml—spring.mvc.problemdetails.enabled: false로 pin.app-bootstrap/.../settings/ProblemDetailDisabledConfigTest.java— shippedapplication.yml이 그 플래그를 literalfalse로 유지하는지 검증 (default flip 회귀 방지).
로컬/dev 검증 (locally-verified)
./gradlew check (전 모듈 test + verifyCleanArchitectureDependencies + ArchUnit CleanArchitectureTest) BUILD SUCCESSFUL (2026-06-01). 검증 테스트: EnvelopeTest/ApiErrorTest/CategoryTest(shared-contract), EnvelopeMetaIntegrationTest(adapter-web standalone MockMvc — meta/category 필드 + leak 차단). 단 운영(prod) 검증은 아직 없음.
feature-api-contract-baseline 의 transport failure envelope 범위는 TransportErrorHandlingTest 로 413/406/415 distinct + 405 Allow header를 검증했고, WorkLogControllerWireTest 로 If-Match mismatch → 412 envelope 흐름을 검증했다. 이 검증은 framework/transport failure를 domain validation과 같은 원인으로 섞는 것이 아니라, 같은 response shape 안에서 status/code/category를 보존하는 범위다.
운영 검증 (prod-verified)
없음. 운영 배포 자체가 존재하지 않는다.
설계 결정 (구현됨 — actually-implemented + locally-verified)
2026-06-01 이전에는 본 섹션 전체가
documented-only였으나 Phase C2로 envelope schema가 코드화·로컬 검증됨. 아래 schema 결정·leak catalog는 이제 코드에 반영돼 있다. 단Retry-After헤더 발행 / 5xx span ERROR 기록은 여전히 seam/stub(owner branch 위임), business rule violation → envelope 변환은 다른 branch 책임이다(아래 명시).
Envelope schema 결정
- success / error 대칭 envelope: 성공도 동일한 top-level shape으로 감싸
success: true/false분기를 client에 단일 규칙으로 제공. error.code(머신리더블 식별자) 와error.category(운영 분류) 를 별도 1급 필드로 분리.error.retryable: boolean을 1급 필드로 승격. client 재시도 정책을 envelope 자체에서 가이드.error.details로 항목 단위 오류(예: validation field error) 를 배열로 운반.meta에requestId,traceId,correlationId를 1급으로 노출 — 로그/트레이스와 응답을 join 가능.
출처: raw/project-notes/ca-skeleton-operational-contract §3 (Structured API Response Contract) / §5 (Exception Ownership Contract) / §6 (Operational Error Category).
5종 envelope 대안 검토 결과
raw/project-notes/ca-skeleton-operational-contract §29 Topic 4에서 다음 5종을 비교 후 custom envelope 채택.
| 후보 | 거부 사유 |
|---|---|
| RFC 7807 ProblemDetail | 실패 전용 평면 shape — success/error 대칭 요구와 구조적 충돌. code/retryable/category 표준 부재로 결국 표준 위에 사실상 custom 레이어 추가가 필요. |
Google rpc.Status |
gRPC/protobuf 결합. HTTP REST 전용에서 Any 디코딩 부담을 client에 전가. CRUD 비중 큰 skeleton에 과한 표현력. |
| JSON:API errors | errors[] + source.pointer는 항목 단위 강점이나 category/retryable 1급 필드 없음. 부분 채택 시 표준성 상실, 완전 채택 시 spec 전체 lock-in. |
| GraphQL errors | HTTP 200 + errors 규약. REST envelope과 패러다임 자체가 다름. CDN/proxy/observability 4xx/5xx 알람과 부조화. |
| Custom envelope (채택) | 표준 client SDK가 0개라는 비용을 감수하는 대신 success/error 대칭 + retryable/category 1급화 + observability 메타 노출이라는 운영 요구를 충족. |
Exception leak 금지 항목 catalog
응답 envelope에 절대 노출 금지로 계약된 항목:
- exception class fully-qualified name
- stack trace 전체 또는 일부
- SQL / SQL fragment / bind parameter
- token / credential / secret 값
- raw request body / raw upstream response body
출처: raw/branch-notes/feature-operational-error-observability-foundation (envelope schema SSOT 및 leak 금지 catalog).
Validation / business rule 매핑
- boundary validation (request DTO 단계): 항목별 오류를
error.details[]에{field, code, message}형태로 매핑. owner = raw/branch-notes/feature-boundary-validation-mapping-contract (actually-implemented—VALIDATION_FAILEDdetails shape). - business rule violation (use case 내부 invariant):
error.category로 분류하고error.details로 세부 위반 정보를 운반. owner = raw/branch-notes/feature-business-rule-validation-contract.
foundation 측 exception → envelope 변환 골격(GlobalExceptionHandler/ErrorResponseFactory)은 구현됨. 위 항목별 매핑 세부(validation field 매핑 / business invariant 분류)는 각 owner branch 책임이다.
Blog-topic ingest: spring-responseentityexceptionhandler-transport-failure-envelope (2026-07-02)
raw/blog-topics/spring-responseentityexceptionhandler-transport-failure-envelope-2026-07-02 는 Spring MVC transport failure를 custom envelope에 태운 경험을 블로그로 풀기 위한 raw seed다. canonical 승격 기준은 다음처럼 정리했다.
- locally-verified 로 말할 수 있는 부분: 413/406/415/405(+
Allow) transport failure와 412 precondition failure가 ca-tmpl envelope shape으로 매핑되고 테스트된다. - source-backed 로 말할 수 있는 부분: 406/415/405/412/413의 HTTP status 의미는 RFC 9110 계열 근거와 기존
api-evolution-and-schemaproject canonical에 연결된다. - project-local implementation 으로 말할 부분: Spring MVC
ResponseEntityExceptionHandler흐름을 깨지 않기 위해 umbrella exception은 protected override로 처리한다는 구현 선택. - 블로그 전 과장 방지: Spring MVC의 모든 예외가 envelope으로 포괄된다고 쓰지 않는다. 검증된 transport failure row와 owner branch 범위로 제한한다.
Blog-topic ingest: operational-error-envelope-meta-category-migration (2026-07-02)
raw/blog-topics/operational-error-envelope-meta-category-migration-2026-06-01 는 기존 {success,data,error,traceId} 응답을 error.category와 meta.{requestId,traceId,correlationId}가 있는 richer envelope로 additive migration한 경험을 블로그로 풀기 위한 raw seed다.
- canonical 반영 범위: verified error envelope 구현 문서에 meta/category migration, enum vocabulary, response meta factory 글감을 연결했다.
- blogify 전 가능 범위: 이 canonical은
verified이므로 blogify 후보가 될 수 있다. - 블로그 전 과장 방지: 운영 배포/운영 검증이 아니라 코드 구현 + 로컬 검증 범위로 제한한다.
- raw/blog-topics/spring-security-filter-layer-error-envelope-2026-06-08: Spring Security filter-layer 인증/인가 실패를 custom
AuthenticationEntryPoint/AccessDeniedHandler에서 같은 envelope shape으로 직렬화하는 글감. 보안 adapter 구현 여부와 heuristic 분류 한계를 재확인한다.
문서/계획만 존재 (documented-only / planned)
Retry-After헤더 발행 (planned): rate-limit owner branch 위임. GlobalExceptionHandler 내 seam/stub 상태. 실제 헤더 발행 로직은 미구현.- 5xx span ERROR 기록 (
planned): distributed-tracing owner branch 위임. span 조립/에러 마킹 로직은 seam/stub 상태. - business rule violation →
error.category매핑 세부 (planned): raw/branch-notes/feature-business-rule-validation-contract 담당. use case 내부 invariant 위반을error.category·error.details로 분류하는 세부 정책은 foundation 측 골격만 존재하고 실제 분류 로직은 미구현.
면접에서 말할 수 있는 범위
자신 있게 답할 수 있는 질문
- ProblemDetail을 채택하지 않은 이유 — 실패 전용 평면 shape이라 success/error 대칭 요구와 구조적으로 충돌하고,
code/retryable/category가 표준 부재라 결국 표준 위에 custom 레이어가 또 필요해진다. retryable을 1급 필드로 둔 의미 — client 재시도 정책을 envelope 자체에서 가이드하기 위함. 단,RetryInfo.retry_delay수준의 actionable delay 정보는 잃는다는 trade-off까지 인지.- validation error를
error.details에 매핑하는 정책의 의도 (boundary vs business rule 구분). - exception leak 금지 항목 catalog와 각 항목이 왜 금지인지.
적당히 답할 수 있는 질문
- Stripe / GitHub / 토스페이먼츠 envelope과 ca-tmpl envelope의 차이점.
- JSON:API
source.pointer와 ca-tmplerror.details[].field표현의 비교.
말할 수 있는 범위 (구현 사실 + 한계)
- "이 envelope을 코드로 구현했는가" — 답: 그렇다 (
actually-implemented+locally-verified).Envelope/ApiError/ResponseMetarecord +GlobalExceptionHandler가 코드에 있고./gradlew check통과. 단 로컬 검증까지다. - "운영에서 어떻게 동작하는가 / 운영 측정값" — 답: 운영(prod) 검증은 없음. 로컬 빌드/테스트 수준까지만.
- "
Retry-After헤더·5xx span ERROR 기록도 동작하는가" — 답: seam/stub 단계. 헤더 발행/span 조립은 rate-limit·distributed-tracing owner branch 위임.
과장 금지 지점
- "ca-tmpl envelope이 표준이다" — ❌. 어떤 IETF/W3C 표준도 success/error 대칭 +
retryable1급 +category1급을 동시에 강제하지 않는다. 자체 결정이다. - "ProblemDetail이 잘못된 설계다" — ❌. 실패 전용 use case (외부 노출 API, RFC 9457 client 생태계 활용)에서는 유효한 선택이다. ca-tmpl의 요구 조합과 맞지 않았을 뿐이다.
- "envelope을 운영에서 검증했다" — ❌. 코드 구현 +
./gradlew check로컬 통과까지(locally-verified)이며, prod 배포·측정은 없다. "구현했다"는 OK, "운영 검증했다"는 과장. - "Stripe/GitHub/토스가 다 custom이니까 표준은 의미 없다" — ❌. 그들은 SDK가 envelope을 흡수하는 전제 위에 동작한다. 표준 미준수가 정당화되는 게 아니라 trade-off가 다른 것뿐이다.
관련 개념
Sources
- raw/project-notes/ca-skeleton-operational-contract §3 Structured API Response Contract / §5 Exception Ownership Contract / §6 Operational Error Category / §29 Topic 4 (5종 envelope 대안 검토)
- raw/branch-notes/feature-operational-error-observability-foundation — envelope schema SSOT, exception leak 금지 catalog
- raw/branch-notes/feature-api-contract-baseline — 413/406/415/405(+
Allow)/412 transport failure envelope 매핑과TransportErrorHandlingTest검증 - raw/blog-topics/spring-responseentityexceptionhandler-transport-failure-envelope-2026-07-02 — transport failure envelope 블로그 글감 raw seed. canonical 반영 범위: verified transport rows + Spring MVC override 경계 + 과장 금지 항목.
- raw/blog-topics/operational-error-envelope-meta-category-migration-2026-06-01 — meta/category migration 블로그 글감 raw seed.
- raw/blog-topics/spring-security-filter-layer-error-envelope-2026-06-08 — Spring Security filter-layer envelope 블로그 글감 raw seed.
- raw/branch-notes/feature-boundary-validation-mapping-contract — boundary validation →
error.details매핑 - raw/branch-notes/feature-business-rule-validation-contract — business invariant →
error.category매핑