fix: 하네스 제거 및 keycloak 문서 보강
This commit is contained in:
@@ -1 +0,0 @@
|
||||
../../../vault/30-knowledge/projects/ca-tmpl/api-error-envelope-design.md
|
||||
@@ -0,0 +1,182 @@
|
||||
---
|
||||
title: ca-tmpl - API Error Envelope 결정 (custom envelope, ProblemDetail 거부)
|
||||
source_type: project
|
||||
status: verified
|
||||
confidence: high
|
||||
tags: [ca-tmpl, api-design, error-handling, actually-implemented, locally-verified]
|
||||
related_projects: [ca-tmpl]
|
||||
last_reviewed: 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을 채택했다.
|
||||
|
||||
```text
|
||||
{
|
||||
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 이름), `retryable` 1급, `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` + `Allow` header), 412(`PRECONDITION_FAILED`) 를 같은 envelope shape으로 반환하되, category/status 의미는 보존한다. Spring MVC `ResponseEntityExceptionHandler` 가 이미 다루는 umbrella exception은 `@ExceptionHandler` 중복 등록이 아니라 protected override로 처리한다.
|
||||
|
||||
ProblemDetail 거부가 **빌드 타임에 강제**된다 (코드 실측):
|
||||
|
||||
- `app-bootstrap/.../architecture/CleanArchitectureTest.java` (ArchUnit) — `org.springframework.http.ProblemDetail` import 금지 규칙(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` — shipped `application.yml`이 그 플래그를 literal `false`로 유지하는지 검증 (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_FAILED` details 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-schema` project 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-tmpl `error.details[].field` 표현의 비교.
|
||||
|
||||
### 말할 수 있는 범위 (구현 사실 + 한계)
|
||||
|
||||
- "이 envelope을 코드로 구현했는가" — **답: 그렇다 (`actually-implemented` + `locally-verified`).** `Envelope`/`ApiError`/`ResponseMeta` record + `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 대칭 + `retryable` 1급 + `category` 1급을 동시에 강제하지 않는다. 자체 결정이다.
|
||||
- **"ProblemDetail이 잘못된 설계다"** — ❌. 실패 전용 use case (외부 노출 API, RFC 9457 client 생태계 활용)에서는 유효한 선택이다. ca-tmpl의 요구 조합과 맞지 않았을 뿐이다.
|
||||
- **"envelope을 운영에서 검증했다"** — ❌. 코드 구현 + `./gradlew check` 로컬 통과까지(`locally-verified`)이며, prod 배포·측정은 없다. "구현했다"는 OK, "운영 검증했다"는 과장.
|
||||
- **"Stripe/GitHub/토스가 다 custom이니까 표준은 의미 없다"** — ❌. 그들은 SDK가 envelope을 흡수하는 전제 위에 동작한다. 표준 미준수가 정당화되는 게 아니라 trade-off가 다른 것뿐이다.
|
||||
|
||||
## 관련 개념
|
||||
|
||||
- [[wiki/concepts/api-error-envelope-design]]
|
||||
|
||||
## 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` 매핑
|
||||
|
||||
## Cluster / 묶음
|
||||
|
||||
<!-- GENERATED: derived-blogs:start -->
|
||||
- [[wiki/blog/ca-tmpl-api-error-envelope-design-2026-07-02]]
|
||||
<!-- GENERATED: derived-blogs:end -->
|
||||
Reference in New Issue
Block a user