603 lines
77 KiB
Markdown
603 lines
77 KiB
Markdown
---
|
|
title: branch / feature-operational-error-observability-foundation
|
|
source_type: branch-note
|
|
status: verified
|
|
last_reviewed: 2026-06-04
|
|
branch: feature-operational-error-observability-foundation
|
|
parent_branch:
|
|
related_projects: [ca-skeleton]
|
|
governing_docs: [wiki/projects/ca-tmpl/api-error-envelope-design, wiki/projects/ca-tmpl/observability-log-metric-trace-runbook]
|
|
tags: [branch, ca-skeleton, error-handling, observability]
|
|
created: 2026-05-21
|
|
target_merge:
|
|
status_label: in-progress
|
|
last_pass: 2026-06-04 (/branch-spec gate — note already mature(D1~D21+Phase C2). governing_docs 추가 + `## Coverage` 섹션 생성; ground-truth 재검증 무드리프트(Category/ResponseMeta/HeaderSanitizer/MdcKeys/RetryAfterAdvisor/ResponseMetaFactory 실재); 게이트: depth **Ready**(Blocking 0) + coverage **Covered**(Blocking 0); depth Should-fix 4건 해소(§엣지 D7 fallback 메커니즘 actually-implemented 기재 / §1 CONFLICT vs DATA_INTEGRITY 분기 경계 Q2 / §7 Q10 4xx=unset 근거 / §8 deprecated yaml Q13); governing doc 2건 최신화(api-error-envelope-design → verified, observability foundation-slice → actually-implemented). 이전: 2026-06-01 Phase C2 구현 완료 — G1~G5/G7 actually-implemented+locally-verified(`./gradlew check` 통과), G6 seam/stub; 미커밋 working tree(사용자 단일 커밋 예정, 중간 SHA git reset 폐기); 파생노트 errors/interviews/blog-topics 캡처. 이전: reinforcement + template 정합 F1~F8/D13~D19; §0 Gap Map + D20 envelope 방향 + D21 Phase C2 분리)
|
|
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-001
|
|
kind: project-work-item
|
|
project: ca-skeleton-operational-contract
|
|
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-001
|
|
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-ERROR-ENVELOPE-001@1]
|
|
refines: []
|
|
overrides: []
|
|
depends_on: []
|
|
contract_packet: 1
|
|
contract_packet_sha256: bd90ec8c5bf60567e7393c1c904ef74f8ecfedfe1ecac9a49a8dd5694b71ecc0
|
|
---
|
|
|
|
# branch: feature-operational-error-observability-foundation
|
|
|
|
> Layer: `raw/branch-notes/` — 운영 실패 분류와 관측성의 첫 기준 branch. 완료 후 `/ingest`로 `wiki/projects/`에만 추출합니다.
|
|
> `status_label`: `in-progress` | `review` | `merged` | `abandoned`
|
|
|
|
<!-- section-id: branch-parent -->
|
|
## 부모 (필수)
|
|
|
|
- **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]]
|
|
|
|
> ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 §3 Structured API Response · §6 Operational Error Category · §7 Retryable · §8 Structured Log / Distributed Tracing · §25 SSOT Owner Map(error envelope / category enum / ID meaning / MDC key) 영역의 결정/근거/금지 사항을 정제한다.
|
|
|
|
### 형제 branch (cross-cite)
|
|
|
|
- [[raw/branch-notes/feature-boundary-validation-mapping-contract]] — envelope custom 채택 공유(D5). `MAPPING_FAILED`/`BATCH_PARTIAL_FAILURE` 를 본 branch enum 의 `VALIDATION` category 로 등록하는 consumer.
|
|
- [[raw/branch-notes/feature-resource-identifier-contract]] — MDC snake_case 정합(`user_id`/`resource_id`), error code never-reuse(D17)와 ID never-reuse(D15) 대칭.
|
|
- [[raw/branch-notes/feature-api-contract-baseline]] — HTTP header registry(`X-Request-Id`/`Retry-After`/`X-RateLimit-*`/`WWW-Authenticate`) producer/cross-owner.
|
|
- [[raw/branch-notes/feature-security-operational-baseline]] — `WWW-Authenticate` 발행(D18) + inbound 헤더 trust 의 보안 측면(D15) owner.
|
|
- [[raw/branch-notes/feature-log-management-contract]] — 본 branch MDC snake_case 표준을 consume(log redaction / PII MDC key 분리).
|
|
- [[raw/branch-notes/feature-metrics-alerting-contract]] — metrics.yaml(error_code cardinality bound) owner. 본 branch enum 을 metric tag dimension 으로 consume.
|
|
- [[raw/branch-notes/feature-distributed-tracing-contract]] — W3C trace context + span 세부(D15/D16) owner. 본 branch 는 ID 의미 + error→span 기록 의도만 정의.
|
|
- [[raw/branch-notes/feature-rate-limit-idempotency-contract]] — 429/`Retry-After` 운영 세부(D13) owner.
|
|
|
|
<!-- GENERATED: branch-contract:start -->
|
|
<!-- section-id: branch-contract-packet -->
|
|
## 브랜치 계약 패킷
|
|
|
|
- **생성 시 프로젝트 개정**: `1`
|
|
- **패킷 스키마**: `contract_packet: 1`
|
|
- **완료 조건**: error·observability 6필드 contract와 contract test가 통과한다
|
|
|
|
<!-- section-id: inherited-project-decisions -->
|
|
### 상속한 프로젝트 결정
|
|
|
|
| Decision Ref | Project Summary | Branch Application | Source |
|
|
|---|---|---|---|
|
|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-ERROR-ENVELOPE-001@1` | foundation이 envelope schema와 error.category enum의 단일 owner다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
|
|
|
|
<!-- section-id: branch-local-decisions -->
|
|
### 브랜치 지역 결정
|
|
|
|
> 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
|
|
|
|
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
|
|---|---|---|---|---|
|
|
|
|
<!-- section-id: declared-overrides -->
|
|
### 선언한 예외
|
|
|
|
| Override ID | Overrides | Reason | Approval | Status |
|
|
|---|---|---|---|---|
|
|
<!-- GENERATED: branch-contract:end -->
|
|
|
|
<!-- section-id: branch-goal -->
|
|
## 목표
|
|
|
|
모든 adapter와 boundary가 같은 실패 언어를 사용하도록 운영 실패 분류 체계를 먼저 고정합니다. 이 branch가 없으면 DB, HTTP, Security, Kafka, Redis, Slack/Email 실패가 각자 다른 방식으로 응답/로그/재시도 정책을 갖게 됩니다.
|
|
|
|
- 이슈:
|
|
- PR:
|
|
|
|
<!-- section-id: branch-scope -->
|
|
## 범위
|
|
|
|
### 포함 범위
|
|
|
|
- structured API response envelope 기준.
|
|
- operational error category/code 기준.
|
|
- retryable/non-retryable 기준 + 재시도 시점의 `Retry-After` surfacing (D13).
|
|
- diagnostic context 기준 + inbound 헤더 sanitization (D14) + trace context trust boundary (D15).
|
|
- requestId/traceId/correlationId/MDC key 기준 + snake↔camel↔kebab 표현 매핑 (D19).
|
|
- operational error 의 trace span 기록 의도 (D16, server-side).
|
|
- error code lifecycle (stability / never-reuse) 기준 (D17).
|
|
- Spring 기본 예외 처리 테스트 기준.
|
|
|
|
### 제외 범위
|
|
|
|
> 의도적으로 제외. 형제 branch 의 owner 결정으로 위임.
|
|
|
|
- DB/JPA 세부 예외 분류.
|
|
- outbound HTTP client 구현.
|
|
- JWT 세부 인증 실패 분류 + `WWW-Authenticate` 헤더 발행 (security-operational-baseline owner — D18 은 cross-cite 만).
|
|
- Kafka/Redis/Slack/Email adapter 구현.
|
|
- error-codes.yaml / mdc-keys.yaml / metrics.yaml 의 실제 row 편집 (registry-governance + ca-tmpl repo).
|
|
- 429/`Retry-After` 운영 세부 + W3C trace context 의 propagation/span 세부 (rate-limit-idempotency / distributed-tracing owner).
|
|
- multi-tenancy 모델 (tenant context-policy branch — 본 branch 의 `tenant_id` MDC key 는 "활성 시" 조건부).
|
|
|
|
## 근거 (필수, 최소 1개+)
|
|
|
|
> 본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 참조.
|
|
|
|
| Source | 정당화하는 결정 |
|
|
|---|---|
|
|
| [[raw/company-tech-blogs/stripe-error-format]] | endpoint dimension 명시 사례 (D3) |
|
|
| [[raw/company-tech-blogs/toss-payments-error-format]] | 한국 컨벤션 reference + UPPER_SNAKE_CASE 사례 (D3/D4) |
|
|
| [[raw/official-docs/problem-detail-rfc-7807]] | IETF 표준이지만 실패 전용, success/error 비대칭 — 거부 근거 (D1) |
|
|
| [[raw/official-docs/spring-problem-detail]] | Spring 6 기본 지원이지만 ca-tmpl envelope과 충돌 (D1) |
|
|
| [[raw/official-docs/google-api-error-format]] | 가장 표현력 풍부, retryable detail 1급 + `ErrorInfo.reason` machine-readable id (D5/D10/D12/D17) |
|
|
| [[raw/official-docs/json-api-errors-spec]] | field error pointer / errors array (D12) |
|
|
| [[raw/official-docs/graphql-errors-spec]] | partial success 1급 |
|
|
| [[raw/company-tech-blogs/github-api-error-format]] | validation code 어휘 사례 (D9) |
|
|
| [[raw/official-docs/rfc9110-http-semantics]] | `Retry-After` semantics(§10.2.3) + 413 temporary(§15.5.14) — retryable surfacing (D13). 401+WWW-Authenticate(§11.6.1) cross-cite (D18) |
|
|
| [[raw/official-docs/tracing-w3c-trace-context-spec]] | `traceparent` 4-field 형식(검증 가능) + propagation/PII 의무 — inbound trace trust boundary (D15) |
|
|
| [[raw/official-docs/owasp-logging-cheat-sheet]] | inbound header MDC 값 sanitization — log injection / CRLF / log forgery (CWE-117) 방어 (D14) |
|
|
| [[raw/official-docs/otel-exceptions-semantic-conventions]] | span exception 이벤트(`exception.type`/`message`/`stacktrace`) + span status ERROR — 서버 측 telemetry 전용 (D16) |
|
|
| [[raw/official-docs/stripe-resource-id-convention]] | opaque string / error message 변경 = backward-compatible → error code 가 안정 계약 표면 (D17) |
|
|
|
|
## 외부 근거 / 대안 조사 (2026-05-22 — Topic 4)
|
|
|
|
본 branch의 custom envelope 결정 (`{success, data, error.{code, category, message, retryable, details}, meta}`, ProblemDetail forbidden)에 대한 외부 source 조사. 비교 분석은 (예정) `wiki/concepts/api-error-envelope-design.md` 참조.
|
|
|
|
- **채택 결정 (custom envelope, success/error 대칭, retryable 1급)**:
|
|
- (어떤 표준도 1:1 매칭 없음 — Stripe/Toss와 가장 유사하나 success flag는 ca-tmpl 고유)
|
|
- [[raw/company-tech-blogs/stripe-error-format]] — endpoint dimension 명시 사례
|
|
- [[raw/company-tech-blogs/toss-payments-error-format]] — 한국 컨벤션 reference
|
|
- **명시적으로 거부한 표준**:
|
|
- [[raw/official-docs/problem-detail-rfc-7807]] — IETF 표준이지만 실패 전용, success/error 비대칭
|
|
- [[raw/official-docs/spring-problem-detail]] — Spring 6 기본 지원이지만 ca-tmpl envelope과 충돌
|
|
- **검토한 대안**:
|
|
- **대안 1: RFC 7807 ProblemDetail** — 위 2개
|
|
- **대안 2: Google rpc.Status (gRPC)** — [[raw/official-docs/google-api-error-format]] (가장 표현력 풍부, retryable detail 1급)
|
|
- **대안 3: JSON:API errors** — [[raw/official-docs/json-api-errors-spec]]
|
|
- **대안 4: GraphQL errors** — [[raw/official-docs/graphql-errors-spec]] (partial success 1급)
|
|
- **대안 5: GitHub custom envelope** — [[raw/company-tech-blogs/github-api-error-format]]
|
|
- **비교 핵심**: ca-tmpl의 `success` flag + `retryable` 1급은 어떤 표준에도 없음. Google rpc.Status만 retryable을 detail로 가짐. ProblemDetail은 실패 전용 평면이라 ca-tmpl의 운영 요구와 구조적 충돌. → ca-tmpl이 ProblemDetail을 거부한 trade-off: 표준 lock-in 회피 + success/error 대칭 + 운영 메타 1급화.
|
|
|
|
## TODO
|
|
|
|
> TODO drained — 결정은 §구현 가이드 (error.category Enum / MDC Key Standard / ID 명명 매핑 / error.details / Retry-After / sanitization+trust / span 기록 / code lifecycle) 참조. 2026-06-01 reinforcement pass 로 D13~D19 추가.
|
|
|
|
## 진행 중 메모
|
|
|
|
- 이 branch는 다른 모든 branch의 선행 계약입니다.
|
|
- 2026-06-01 reinforcement pass: 다관점 브레인스토밍으로 8개 사각(F1~F8) 식별 → D13~D19 추가 + template 구조 정합 + 공식문서 2건(OWASP Logging, OTel Exceptions) raw 캡처. **4개 선행 계약(skeleton-package-blueprint / architecture-enforcement / boundary-validation-mapping / resource-identifier) 의 결정과 충돌 없음 — F1 은 부모 §6 stale 재정합(안정화), F2~F8 은 additive 또는 sibling/owner cross-cite.** 설계: `docs/superpowers/specs/2026-06-01-operational-error-observability-foundation-reinforcement-design.md`.
|
|
- F1 (부모 §6/§8 정합): 부모 project-note §6 의 stale 13-category 목록을 본 branch 의 canonical 10-enum 으로 정합.
|
|
- 2026-06-01 코드 검증 정정: 이전 §0 Realization Map 이 "boundary 가 이미 구현" 이라 과장(overclaim)했으나, ca-tmpl 코드 실측 결과 boundary 는 *단순 shape*(flat traceId / category 없는 error / camelCase MDC)만 구현 — foundation 계약의 full target(meta 객체/category 1급/snake_case/correlation_id/sanitization/Retry-After/span ERROR)은 **미구현(G1~G7)**. §0 을 "Realization Gap Map"(진입점 + GAP 표)으로 교정. 결정: **코드는 Phase C2 로 보류(D21), envelope 방향은 foundation meta+category(D20)**. 코드 미수정.
|
|
- 2026-06-01 registry 검증 (re-tag 권고 후속): `ca-tmpl/docs/registries/error-codes.yaml` (49 codes) 를 read-only 검증한 결과 **이미 10-enum 으로 완전 정리됨** — 옛 category(AUTHENTICATION/PERSISTENCE/CACHE 등) 0건, category 분포가 부모 §21 L811 과 정확히 일치. **per-code 재태그는 no-op(이미 완료, "Phase A 4차 audit Conflict 13 해소").** 따라서 stale 했던 유일 artifact 는 부모 §6 본문이었고(이미 정합), yaml 은 원래부터 정확. 단 검증 중 이상치 1건(`AUTH_KID_UNKNOWN` retryable=false + retry_after_seconds=5) 발견 → 사용자 결정으로 `retryable: true` 적용(2026-06-01, JWKS 키 회전 가정, 가역 — 키 고정 시 false 복귀). Claims To Verify 에 `locally-verified` 로 등재.
|
|
|
|
## 결정 사항
|
|
|
|
- 2026-05-21: `ProblemDetail`은 사용하지 않고 자체 envelope 응답을 사용. (D1)
|
|
- 2026-05-21: domain/business-specific exception보다 operational failure classification을 우선. (D2)
|
|
- 2026-05-22: response envelope field는 `success`, `data`, `error`, `meta`를 기본값으로 둠. (D3)
|
|
- 2026-05-22: error code는 `UPPER_SNAKE_CASE`, category는 coarse-grained operational category로 둠. (D4)
|
|
- 2026-05-22: client-safe message와 internal diagnostic context는 같은 객체에 섞지 않음. (D5)
|
|
- 2026-05-22: 이 branch가 error envelope schema, `error.category` enum, `requestId`/`traceId`/`correlationId` 의미, MDC/log key 표준의 SSOT owner. (D6)
|
|
- 2026-05-22: tracing disabled 상태에서도 `meta.traceId`는 누락하지 않음. 실제 trace가 없으면 generated opaque id를 사용하고 `trace.sampled=false`를 diagnostic context/log에만 남김. (D7)
|
|
- 2026-05-22: `requestId`는 inbound HTTP request 단위 식별자, `traceId`는 distributed trace 상관관계 식별자, `correlationId`는 business-neutral workflow 식별자로 final 정의. (D8)
|
|
- 2026-05-22: 본 branch는 `error.category` enum과 MDC Key 표준의 SSOT. **실 error code list (AUTH_TOKEN_EXPIRED, DB_UNIQUE_VIOLATION 등)는 별도 `ca-tmpl/docs/registries/error-codes.yaml`에 통합 SSOT로 작성 (Phase B). 본 branch는 그 yaml의 schema/category 매핑만 정의. (D9)
|
|
- 2026-05-22: `error.category` enum 10개 final. (D10)
|
|
- 2026-05-22: MDC Key Standard = snake_case 강제. (D11)
|
|
- 2026-05-22: `error.details` JSON shape = `{field, rejectedValue, code, message}`. (D12)
|
|
- 2026-06-01: (D13) retryable 응답은 재시도 시점을 `Retry-After` 헤더로 surface. 503/TRANSIENT_DEPENDENCY 는 `Retry-After` MUST(RFC 9110 §10.2.3), 429/RATE_LIMIT 은 `Retry-After`(+ `X-RateLimit-*` 권고) — 429 세부는 rate-limit-idempotency owner. / 이유: envelope `error.retryable: true` 만으로는 client 가 *언제* 재시도할지 모름. / 검토한 대안: (a) envelope 에 `retryAfterSeconds` 필드 추가 — HTTP 표준 헤더 중복, (b) 헤더만 — 채택. / 근거: [[raw/official-docs/rfc9110-http-semantics]].
|
|
- 2026-06-01: (D14) inbound header (`X-Request-Id`, `X-Correlation-Id`) 값을 MDC/로그에 반영할 때 CR/LF/구분자 sanitization 의무 (log injection / log forgery 방어). / 이유: client 제공 값이 그대로 로그에 들어가면 CWE-117 log injection. / 검토한 대안: (a) 구조화 JSON 로깅만 신뢰 — 필드 smuggling/길이 폭주 잔존, (b) sanitization + 구조화 로깅 병행 — 채택. / 근거: [[raw/official-docs/owasp-logging-cheat-sheet]].
|
|
- 2026-06-01: (D15) client 제공 `traceparent`/`X-Request-Id`/`X-Correlation-Id` 의 trust boundary — edge 에서 format 검증 + length cap, 무효 시 재생성(traceparent 는 새 trace 시작). / 이유: 외부 입력을 무검증으로 trace/MDC 에 채택하면 위조·과대 헤더 risk. / 검토한 대안: (a) 항상 재생성(client 값 무시) — cross-service correlation 손실, (b) 항상 신뢰 — 위조 risk, (c) 검증 후 수용/무효 시 재생성 — 채택. trust 의 보안 세부는 security-operational-baseline, propagation 세부는 distributed-tracing owner. / 근거: [[raw/official-docs/tracing-w3c-trace-context-spec]].
|
|
- 2026-06-01: (D16) operational `INTERNAL`(5xx) 오류는 server 측 span 에 `exception` 이벤트(`exception.type`/`message`/`stacktrace`) 기록 + span status ERROR 설정. client HTTP 응답에는 stack trace 미포함(D5/판정 기준 Forbidden). / 이유: 관측성 = log + trace + metric. ID 전파만으로는 error 가 trace 에 안 남음. / 검토한 대안: (a) log 에만 stack — trace 상관 단절, (b) span event + status ERROR — 채택. span 세부는 distributed-tracing owner. / 근거: [[raw/official-docs/otel-exceptions-semantic-conventions]].
|
|
- 2026-06-01: (D17) `error.code` 는 안정적 공개 API 계약 — append-only, rename/재사용 금지(never-reuse), 폐기는 compatibility 절차(Deprecation/Sunset). / 이유: code 가 client 분기/알림/runbook 에 박힌 후 rename/재사용은 breaking + audit 혼선. resource-identifier D15(ID never-reuse)와 대칭. / 검토한 대안: (a) code 자유 변경 — client 깨짐, (b) append-only + deprecation 절차 — 채택. deprecation 절차 owner = api-compatibility. / 근거: [[raw/official-docs/stripe-resource-id-convention]], [[raw/official-docs/google-api-error-format]].
|
|
- 2026-06-01: (D18) `AUTH`(401) 응답은 `WWW-Authenticate` 헤더 동반(RFC 9110 §11.6.1 MUST). / 이유: bare 401 은 HTTP 표준 위반. / 검토한 대안: 없음(표준 의무). 헤더 발행 정책 owner = security-operational-baseline — 본 branch 는 envelope/category 측 cross-cite 만. / 근거: [[raw/branch-notes/feature-security-operational-baseline]] (§25 owner) + RFC 9110 §11.6.1.
|
|
- 2026-06-01: (D19) 동일 식별자(request/trace/correlation/tenant)의 **표현 계층별 명명 매핑 명시** — MDC = `snake_case`(`request_id`), envelope meta = `camelCase`(`meta.requestId`), HTTP header = `kebab-case`(`X-Request-Id`) / W3C lowercase(`traceparent`). / 이유: branch-note 단독 독해 시 "MDC snake_case 강제" 와 "envelope `meta.requestId`(camel)" 가 모순처럼 보임 — 의도적 매핑임을 명문화. / 검토한 대안: 단일 case 통일 — HTTP/W3C/JSON 관례와 충돌. envelope camelCase owner = schema-serialization. / 근거: [[raw/project-notes/ca-skeleton-operational-contract]] §21 + §25.
|
|
- 2026-06-01: (D20) **envelope shape 충돌 해소 방향 = foundation `meta.{requestId,traceId,correlationId}` 객체 + `error.category` 채택** (G1/G2/G7). 현재 코드의 boundary flat `traceId` shape 는 Phase C2 에서 마이그레이션. / 이유: 부모 §3 + §21(L818-833)이 meta.* envelope field 를 확정하고 §25 가 envelope schema 소유를 foundation 에 부여 — flat shape 는 미완 subset. boundary 결정 D5(ProblemDetail 거부)/D6(success-error 대칭)은 *불변* (richer shape 는 additive, 결정 reversal 아님). / 검토한 대안: 계약을 flat shape 로 하향 수정 — 관측성 계약(meta.{}) 포기라 기각. / 근거: 부모 §3/§21/§25 + 2026-06-01 코드 검증(§0 GAP Map).
|
|
- 2026-06-01: (D21) **Phase C2 코드 구현(G1~G7 해소)은 본 reinforcement 패스 범위 밖 — 별도 `writing-plans` 로 분리**. / 이유: Envelope/ApiError shape 변경 + MDC camel→snake 는 boundary 의 realized 코드/테스트(`VirtualThreadMdcE2ETest`/`GlobalExceptionHandlerTest`/`WorkLogController`)를 깨므로 조율된 마이그레이션 plan + 검증 체크포인트 필요 — ad-hoc 금지. cross-owned 항목(Retry-After 헤더/WWW-Authenticate/span 조립)은 owner branch 가 구현, foundation 은 hook + cross-cite stub. / 검토한 대안: 지금 전면 구현 — blast radius 무계획 처리 위험으로 기각. / 근거: 사용자 결정 2026-06-01.
|
|
|
|
## 결정-근거 매핑
|
|
|
|
> 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시한다.
|
|
> `Decision ID` 는 이 branch-note 안에서 안정적으로 유지한다.
|
|
> Company-tech-blog evidence 는 `company-case-study` 로만 표기 — official best practice 아님.
|
|
|
|
| Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk |
|
|
|---|---|---|---|---|
|
|
| D1 | `ProblemDetail` 사용 거부, 자체 envelope 응답 사용 | `raw/official-docs/problem-detail-rfc-7807.md#RFC7807-C1`, `raw/official-docs/problem-detail-rfc-7807.md#RFC7807-C2`, `raw/official-docs/spring-problem-detail.md#SPRING-PD-C1`, `raw/official-docs/spring-problem-detail.md#SPRING-PD-C3` | `official-standard` (RFC 7807 의 `application/problem+json` + `type` MUST 식별자 — ca-tmpl 의 success/error 대칭 + retryable 1급 필드와 구조적 충돌) + `official-vendor-doc` (Spring ProblemDetail 의 RFC 9457 representation) | RFC 7807/9457 거부의 trade-off: 표준 lock-in 회피 vs 표준 호환성 손실. RFC 7807 `extensions` 메커니즘 (`RFC7807-C3`) 으로 retryable/category 표현 가능했음 |
|
|
| D2 | domain/business-specific exception 보다 operational failure classification 우선 | UNSUPPORTED_DECISION (DDD / clean architecture 일반 원칙 — 외부 official-standard / official-vendor-doc 직접 근거 없음) | N/A | 내부 정책으로만 표현. wiki 추출 시 `wiki/concepts/clean-architecture-*` 류 별도 근거 필요 |
|
|
| D3 | response envelope field = `success`, `data`, `error`, `meta` default | UNSUPPORTED_DECISION (어떤 표준도 `success` flag 를 직접 정의하지 않음. `raw/company-tech-blogs/stripe-error-format.md` / `raw/company-tech-blogs/toss-payments-error-format.md` 는 company-case-study — 공식 best practice 아님) | `company-case-study` (Stripe `STRIPE-ERR-C5` 4-type enum + Toss `TOSS-ERR-C1` `{code,message}` 2-field — ca-tmpl 의 envelope 는 양쪽 모두와 다름) | success flag 의 raw source 0건. ca-tmpl 자체 design — 면접/외부 공개 시 "내부 design choice" 로만 표현 |
|
|
| D4 | error code = `UPPER_SNAKE_CASE`, category = coarse-grained operational | `raw/company-tech-blogs/toss-payments-error-format.md#TOSS-ERR-C3`, `raw/company-tech-blogs/toss-payments-error-format.md#TOSS-ERR-C4`, `raw/company-tech-blogs/toss-payments-error-format.md#TOSS-ERR-C5` (UPPER_SNAKE_CASE 사례 — `UNAUTHORIZED_KEY`, `INVALID_REQUEST`, `ALREADY_PROCESSED_PAYMENT` 등) | `company-case-study` (Toss 의 코드 형식 사례 — 공식 표준 아님) | Toss case 는 vendor convention. GitHub `GH-ERR-C4` 는 lowercase (`missing`, `invalid`) — 업계 통일 컨벤션 없음. UPPER_SNAKE_CASE 결정의 spec 근거 부재 |
|
|
| D5 | client-safe message 와 internal diagnostic context 분리 (같은 객체에 섞지 않음) | `raw/official-docs/problem-detail-rfc-7807.md#RFC7807-C5` (`detail` member 는 client 가 정정하는 데 도움 — debugging 정보 제공이 아닌) + `raw/official-docs/google-api-error-format.md#GOOG-ERR-C2` (`message` 는 developer-facing debug message) | `official-standard` + `official-vendor-doc` | RFC 7807 `ought to` 는 `SHOULD` 보다 약한 어조. Google `GOOG-ERR-C2` 는 developer-facing 정의 — end-user 메시지 분리 자체는 ca-tmpl 내부 정책 |
|
|
| D6 | 본 branch 가 error envelope schema, `error.category` enum, requestId/traceId/correlationId 의미, MDC/log key 표준의 SSOT owner | UNSUPPORTED_DECISION (SSOT ownership 분할은 내부 운영 정책) | N/A | branch ownership 분할은 외부 표준 인용 대상 아님. 부모 §25 SSOT Owner Map 과 정합 |
|
|
| D7 | tracing disabled 상태에서도 `meta.traceId` 누락 금지 — generated opaque id 사용 + `trace.sampled=false` diagnostic | UNSUPPORTED_DECISION (인용된 official-docs 에 "tracing disabled 시 opaque id 생성" 정책 직접 근거 없음) | N/A | OTel SDK noop tracer 동작 별도 verbatim 필요 |
|
|
| D8 | requestId/traceId/correlationId 의 정확한 의미 final 정의 (request 단위 / distributed trace 상관 / business-neutral workflow) | `raw/official-docs/tracing-w3c-trace-context-spec.md#W3C-TC-C2` (traceId 의 W3C 정의만 부분 지지) + UNSUPPORTED_DECISION (requestId / correlationId 의미는 ca-tmpl 내부 컨벤션 — 외부 표준 없음) | `official-standard` (traceId only) | requestId / correlationId 는 vendor / 컨벤션 별. ca-tmpl 내부 정의로만 표현 |
|
|
| D9 | 실 error code 카탈로그는 `ca-tmpl/docs/registries/error-codes.yaml` 통합 SSOT (Phase B) — 본 branch 는 schema/category 매핑만 | `raw/company-tech-blogs/github-api-error-format.md#GH-ERR-C4` (GitHub 6개 validation error code 어휘 — 외부 카탈로그 사례) | `company-case-study` | GitHub 6-code 어휘는 vendor convention. ca-tmpl 의 yaml registry 패턴 자체는 외부 표준 인용 없음 |
|
|
| D10 | `error.category` enum 10개 (VALIDATION/AUTH/AUTHZ/NOT_FOUND/CONFLICT/RATE_LIMIT/TRANSIENT_DEPENDENCY/PERMANENT_DEPENDENCY/DATA_INTEGRITY/INTERNAL) | `raw/official-docs/google-api-error-format.md#GOOG-ERR-C1` (Google `google.rpc.Code` enum 사례 — NOT_FOUND=5 등 코드 매핑) | `official-vendor-doc` (Google AIP-193) | Google `Code` enum 은 16개 (OK, CANCELLED, INVALID_ARGUMENT 등) — ca-tmpl 10개와 1:1 아님. ca-tmpl enum 은 자체 design. **부모 §6 의 stale 13-목록은 본 enum 으로 정합 필요 (F1) — §21 L814 매핑 기준** |
|
|
| D11 | MDC Key Standard = snake_case 강제 (`request_id`, `trace_id`, `span_id`, `correlation_id`, `tenant_id`, `user_principal`) | UNSUPPORTED_DECISION (인용된 official-docs 에 snake_case MDC key 강제 표준 없음 — Logback / SLF4J 는 컨벤션 미강제) | N/A | ECS 는 dot notation (`trace.id`), Micrometer 는 dot.case — ca-tmpl 의 snake_case 는 자체 design choice. envelope/header 표현은 D19 매핑 |
|
|
| D12 | `error.details` JSON shape = `{field, rejectedValue, code, message}` (validation field error) | `raw/official-docs/json-api-errors-spec.md#JSONAPI-ERR-C3` (`source.pointer` = JSON Pointer for field location), `raw/official-docs/json-api-errors-spec.md#JSONAPI-ERR-C2`, `raw/official-docs/google-api-error-format.md#GOOG-ERR-C5` (`BadRequest`, `PreconditionFailure` typed payloads) | `official-standard` (JSON:API) + `official-vendor-doc` (Google AIP-193) | ca-tmpl 의 `field` 는 dot path 또는 JSON pointer 둘 다 허용 — JSON:API `source.pointer` 는 RFC 6901 JSON Pointer 만. spec 일치 아님 |
|
|
| D13 | retryable 응답은 `Retry-After` 헤더로 재시도 시점 surface (503/TRANSIENT_DEPENDENCY MUST, 429/RATE_LIMIT + `X-RateLimit-*` 권고) | `raw/official-docs/rfc9110-http-semantics.md#RFC9110-C21` (Retry-After = follow-up request 대기 시간, 503 시 unavailable 예상 시간) + `raw/official-docs/rfc9110-http-semantics.md#RFC9110-C6` (413 temporary → Retry-After SHOULD) | `official-standard` (503/3xx) | 429 의 Retry-After 는 RFC 6585 §4 (본 raw 미보관 — rate-limit-idempotency owner). `X-RateLimit-*` 는 비표준 관례 (headers.yaml 등록). **UNSUPPORTED_IMPL_DECISION**: `retry_after_seconds` 값 자체는 error-codes.yaml project-decision |
|
|
| D14 | inbound HTTP header (`X-Request-Id`, `X-Correlation-Id`) 값을 MDC 에 반영할 때 CR / LF / 구분자 문자를 strip 하는 sanitization 을 의무화 (log injection / log forgery 방어) | `raw/official-docs/owasp-logging-cheat-sheet.md#OWASP-LOG-C1` (외부 trust zone 데이터는 untrusted), `raw/official-docs/owasp-logging-cheat-sheet.md#OWASP-LOG-C3` (CR/LF/delimiter sanitization 명시), `raw/official-docs/owasp-logging-cheat-sheet.md#OWASP-LOG-C5` (CWE-117 명시 위협) | `official-reference` (OWASP Cheat Sheet Series — 규범적 국제표준 아님, engineering guidance) | **UNSUPPORTED_IMPL_DECISION**: sanitization 의 구체적 구현(regex, allowlist charset, 최대 길이)은 OWASP 가 직접 규정하지 않음 — 길이/charset 제한은 사용자 임의 trade-off |
|
|
| D15 | client 제공 `traceparent`/`X-Request-Id`/`X-Correlation-Id` 의 trust boundary — format 검증 + length cap, 무효 시 재생성 (traceparent 무효 시 새 trace) | `raw/official-docs/tracing-w3c-trace-context-spec.md#W3C-TC-C2` (traceparent 4-field 형식 — 검증 가능) + `raw/official-docs/tracing-w3c-trace-context-spec.md#W3C-TC-C5` (propagation MUST + tracestate PII 금지 MUST NOT) | `official-standard` (format/propagation) + UNSUPPORTED_IMPL_DECISION (trust-vs-continue / length cap / 무효→재생성 detail) | 무효 traceparent 재시작 정책 + tracestate 32-member/길이 한계는 W3C 별도 섹션 미보관. trust 의 보안 측면 = security-operational-baseline owner, propagation/span 세부 = distributed-tracing owner |
|
|
| D16 | operational `INTERNAL`(5xx) 오류 발생 시 server 측 span 에 `exception` 이벤트(`exception.type`/`message`/`stacktrace`) 기록 + span status ERROR. client HTTP 응답에는 stack trace 미포함 | `raw/official-docs/otel-exceptions-semantic-conventions.md#OTEL-EXC-C1` (event name MUST be `exception`) + `#OTEL-EXC-C2` (exception.type/message/stacktrace attribute) + `#OTEL-EXC-C4` (오류 시 SHOULD set span status ERROR) + `#OTEL-EXC-C6` (Application developer 가 status 자유 설정 가능) | `official-vendor-doc` (OTel Semantic Conventions) | (a) `exception.stacktrace` 를 client 응답에서 제외해야 한다는 OTel 직접 근거 없음 — ca-tmpl 자체 보안 정책 (D5, Forbidden); (b) HTTP 5xx↔span ERROR 매핑은 HTTP semconv 별도; (c) exceptions-spans 사양 deprecated → exceptions-in-logs 전환 시 재검토. span 세부 = distributed-tracing owner |
|
|
| D17 | `error.code` 는 안정적 공개 API 계약 — append-only, rename/재사용 금지(never-reuse), 폐기는 compatibility 절차 | `raw/official-docs/stripe-resource-id-convention.md#STRIPE-C2` (opaque string + error message 변경 = backward-compatible 분류 → code 가 안정 계약 표면) + `raw/official-docs/google-api-error-format.md#GOOG-ERR-C4` (모든 error 에 `ErrorInfo` 필수 — machine-readable 식별자) | `company-case-study` + `official-vendor-doc` + UNSUPPORTED_DECISION (never-reuse 자체는 ca-tmpl 운영 정책 — resource-identifier D15 ID never-reuse 대칭) | deprecation 절차(Deprecation/Sunset 헤더 발행 시점) = api-compatibility owner. STRIPE-C2 는 message 변경이 호환임을 말할 뿐 code 불변을 직접 증명하지 않음 (대비 관계로 인용) |
|
|
| D18 | `AUTH`(401) 응답은 `WWW-Authenticate` 헤더 동반 (RFC 9110 §11.6.1 MUST) | [[raw/branch-notes/feature-security-operational-baseline]] (WWW-Authenticate 발행 owner — 부모 §25 L1086) + RFC 9110 §11.6.1 (raw claim 미보관 — 필요 시 `rfc9110-http-semantics.md` 보강) | `cross-branch-SSOT` | 본 branch 는 envelope/category 측 cross-cite 만 — 헤더 발행 정책은 security-operational-baseline owner. 401 enum row 가 bare 401 을 암시하지 않도록 §구현 가이드 §9 에 명시 |
|
|
| D19 | 동일 식별자의 표현 계층별 명명 매핑 — MDC=snake_case / envelope meta=camelCase / HTTP header=kebab-case(W3C lowercase) | [[raw/project-notes/ca-skeleton-operational-contract]] §21 (`request_id`↔`meta.requestId`↔`X-Request-Id` 매핑 등록) + §25 (envelope camelCase owner = schema-serialization) | `project-ssot` | **UNSUPPORTED_IMPL_DECISION**: 매핑 방향/케이스 선택 자체는 registry + schema-serialization 분담 결정 — 외부 표준 단일 근거 없음. resource-identifier §9 의 `user.id`/`resource.id`(dot) illustration 은 snake(`user_id`/`resource_id`)로 conform 필요 |
|
|
| D20 | envelope shape 충돌 해소 = foundation `meta.{requestId,traceId,correlationId}` 객체 + `error.category` 채택 (G1/G2/G7). 현재 코드 flat `traceId` 는 Phase C2 마이그레이션 | [[raw/project-notes/ca-skeleton-operational-contract]] §3 (성공/실패 응답 meta.* 명세) + §21 L818-833 (Response Envelope 요약: `meta.requestId`/`meta.traceId`/`meta.correlationId` 필수) + §25 (envelope schema owner = foundation) | `project-ssot` | boundary realized 코드/테스트(`VirtualThreadMdcE2ETest`/`GlobalExceptionHandlerTest`/`WorkLogController`) 마이그레이션 비용 — Phase C2 plan(D21)에 포함. boundary 결정 D5/D6 은 불변(additive). 2026-06-01 코드 검증(§0 (B) GAP Map) 근거 |
|
|
| D21 | Phase C2에서 G1~G5/G7 구현과 로컬 검증을 완료했다. G6 및 cross-owned Retry-After 헤더/WWW-Authenticate/span 조립은 각 owner 구현 + foundation hook/cross-cite stub으로 유지한다 | UNSUPPORTED_DECISION (구현 phasing 은 ca-tmpl 운영 결정 — 외부 근거 대상 아님. 사용자 결정 2026-06-01) | N/A | G6와 cross-owned 항목은 owner 계약이 갱신될 때 통합 검증 필요 |
|
|
|
|
## 구현 가이드
|
|
|
|
> *결정* 이 "*무엇*" 이라면 본 §는 "*어디에 어떻게*" 의 사전 명세. 각 sub-section 은 `Decision ID` + `Supporting Claim ID` 를 Trace (R1). 근거 없는 detail 은 `UNSUPPORTED_IMPL_DECISION` (R2). 본 branch 범위 밖은 sibling/owner SSOT 로 이관 (R3).
|
|
|
|
### 0. Realization Gap Map (현재 코드 진입점 vs 계약 target)
|
|
|
|
> **Trace**: 본 branch 는 *schema/enum/ID 의미 SSOT (design)*. **2026-06-01 코드 검증 정정**: 이전 본 §0 은 "boundary 5·6차 패스가 이미 구현" 이라 적었으나, 실제 코드 확인 결과 **boundary 가 구현한 것은 더 단순한 shape** 이고 본 foundation 계약의 full target 은 **미구현 (Phase C2)**. `Envelope.java`/`ApiError.java`/`OperationalError.java` 의 javadoc 도 스스로를 *boundary D5/D6 소유* 라 명시 — foundation 의 meta 객체/category 1급/snake_case 는 아직 없음. 본 §0 은 *진입점 위치* + *계약 vs 코드 GAP* 의 정직한 지도다 (CLAUDE.md §6: `documented-only` 를 `actually-implemented` 로 표기 금지).
|
|
>
|
|
> - **증거 등급**: 진입점 클래스 = `actually-implemented` (존재). 계약 target shape = `planned` (Phase C2 미구현).
|
|
|
|
**(A) 진입점 클래스 (존재 — boundary 패스가 단순 shape 로 구현):**
|
|
|
|
| 계약 요소 | 진입점 클래스 (ca-tmpl) | 현재 구현 상태 |
|
|
|---|---|---|
|
|
| envelope schema | `shared-contract` `response/Envelope<T>` + adapter-web `envelope/EnvelopeBodyAdvice` | `actually-implemented` (단, 단순 shape — (B) 참조) |
|
|
| 예외 → dispatch | adapter-web `error/GlobalExceptionHandler` + `error/ErrorResponseFactory` | `actually-implemented` |
|
|
| error code | `shared-contract` `error/OperationalError` enum + `error/ApiErrorCode` interface | `actually-implemented` (code 목록 — category 개념 부재) |
|
|
| MDC 생성/set/clear | adapter-web `RequestLoggingFilter` | `actually-implemented` (camelCase, X-Request-Id only) |
|
|
|
|
**(B) 계약 target vs 현재 코드 GAP — Phase C2 해소 완료 (2026-06-01):** G1~G5/G7 = `actually-implemented` `locally-verified`(`./gradlew check` 통과), G6 = seam/stub(owner 위임).
|
|
|
|
| GAP | 계약/레지스트리 요구 | 해소 상태 | 증거 (ca-tmpl) | 관련 결정 |
|
|
|---|---|---|---|---|
|
|
| G1 | `error.category` (10-enum) 응답 노출 | ✅ `ApiError` 에 `category` 필드 + `ErrorResponseFactory` 가 `code.category().name()` 주입 | `shared-contract/response/ApiError.java`, `adapter-web/error/ErrorResponseFactory.java` | D10 |
|
|
| G2 | `meta.{requestId,traceId,correlationId}` 객체 | ✅ `ResponseMeta` record + `Envelope`/`BulkEnvelope` 가 flat `traceId`→`meta` 로 교체 | `shared-contract/response/ResponseMeta.java`, `Envelope.java`, `BulkEnvelope.java` | D19 / 판정기준 Required fields |
|
|
| G3 | MDC snake_case (`request_id`/`trace_id`/`correlation_id`) | ✅ `MdcKeys`(snake) + `RequestLoggingFilter` 전환 + logback `includeMdcKeyName` snake | `adapter-web/observability/MdcKeys.java`, `RequestLoggingFilter.java`, `app-bootstrap/logback-spring.xml` | D11 / D19 / mdc-keys.yaml |
|
|
| G4 | `correlation_id` / `X-Correlation-Id` 처리 | ✅ 필터가 `X-Correlation-Id` 수신/생성 + MDC/응답헤더 반영 | `RequestLoggingFilter.java` | D8 / mdc-keys.yaml |
|
|
| G5 | D14 inbound 헤더 CR/LF sanitization | ✅ `HeaderSanitizer`(CR/LF·제어문자 strip + length cap), 필터가 inbound id 에 적용 | `adapter-web/observability/HeaderSanitizer.java`, `RequestLoggingFilter.java` | D14 |
|
|
| G6 | D13 Retry-After (503/429) + D16 5xx span ERROR | ⏸ seam/stub 만 (`RetryAfterAdvisor`) — 헤더 발행/span 조립은 owner branch(rate-limit/distributed-tracing), tracing 의존성 부재 | `adapter-web/observability/RetryAfterAdvisor.java` | D13 / D16 / D21 |
|
|
| G7 | `error.category` 10-enum 개념 | ✅ `Category` enum 10값 + `ApiErrorCode.category()` + `OperationalError` 매핑 | `shared-contract/error/Category.java`, `ApiErrorCode.java`, `OperationalError.java` | D10 |
|
|
|
|
> **Blast radius (실현됨)**: GAP 해소가 boundary 의 realized shape(flat `traceId`/category 없는 error/camelCase MDC)를 바꾸면서 `VirtualThreadMdcE2ETest`/`GlobalExceptionHandlerTest`/`EnvelopeBodyAdviceTest`/`BulkEnvelopeTest`/`WorkLogController`/`PortfolioErrorCode` 가 깨졌고 전부 마이그레이션. boundary 결정 D5/D6 은 불변(richer shape 는 additive). 방향 = **D20**, phasing = **D21**(`writing-plans` 로 plan 작성 후 ca-implementer + 리뷰체인으로 착수, 이후 사용자 요청으로 미커밋 직접 구현 전환). 트러블슈팅: [[raw/errors/webmvctest-nested-springbootconfiguration-context-pollution-2026-06-01]].
|
|
|
|
### 1. `error.category` Enum (final)
|
|
|
|
> **Trace**: D10 + `GOOG-ERR-C1` (google.rpc.Code enum 사례). HTTP status / retryable default 는 enum 의 운영 가정.
|
|
>
|
|
> - **F1 부모 §6 정합**: 부모 project-note §6 의 stale 13-category 목록(`AUTHENTICATION/AUTHORIZATION/PERSISTENCE/DEPENDENCY/SECURITY/MESSAGE/CACHE/NOTIFICATION`)은 본 10-enum 으로 정합 필요. 매핑 = `AUTHENTICATION→AUTH`, `AUTHORIZATION→AUTHZ`, `PERSISTENCE→{DATA_INTEGRITY, TRANSIENT_DEPENDENCY}` (부모 §21 L814 "Conflict 13 해소"). per-code category 는 error-codes.yaml authoritative.
|
|
> - **retryable 출처 (Q1, 구현 명확화)**: 런타임 `error.retryable` 값은 **error-codes.yaml 의 per-code row 가 authoritative** — 아래 표의 retryable 은 *yaml 작성 default* 일 뿐 런타임 분기가 아니다. 구현자는 category 로 retryable 을 *계산하지 않고* code row 값을 읽는다. "CONFLICT 의 lock-only 는 true" 도 런타임 category 분기가 아니라 **별도 code** (예: `OPTIMISTIC_LOCK_CONFLICT` = category CONFLICT, retryable=true) 로 표현한다.
|
|
> - **UNSUPPORTED_IMPL_DECISION**: HTTP status / retryable default 매핑값(예: PERMANENT_DEPENDENCY=502, RATE_LIMIT retryable=true)은 ca-tmpl 운영 가정 — 일부는 incident 회고로 재검토 (Claims To Verify 참조).
|
|
> - **CONFLICT(409) vs DATA_INTEGRITY(409) 런타임 분기 (Q2, UNSUPPORTED_IMPL_DECISION + 경계)**: 두 category 모두 409 라 *어떤 persistence 예외가 어느 쪽인가*는 enum 만으로 안 갈린다. 본 branch 는 **분기 기준이 아니라 enum 만 소유** — 실제 JPA 예외 → code 매핑(예: `OptimisticLockingFailureException`/serialization/deadlock 계열 → CONFLICT, unique/FK/null/check 위반 → DATA_INTEGRITY)은 **per-code 로 error-codes.yaml + [[raw/branch-notes/feature-persistence-failure-baseline]] (persistence adapter owner) 책임**. 코드 ground truth: `OperationalError.java` javadoc 이 optimistic-lock 계열을 `CONFLICT`(= `DB_SERIALIZATION_FAILURE`/`DB_DEADLOCK` 와 같은 family)로 명시. 구현자는 category 로 분기를 *계산하지 않고* persistence adapter 가 던지는 code 의 `category()` 를 읽는다.
|
|
|
|
| value | 의미 | HTTP status default | retryable default |
|
|
|-------|------|---------------------|-------------------|
|
|
| VALIDATION | client request 형식·shape 오류 | 400 | false |
|
|
| AUTH | 인증 실패 | 401 | false |
|
|
| AUTHZ | 권한 부족 | 403 | false |
|
|
| NOT_FOUND | 자원 없음 | 404 | false |
|
|
| CONFLICT | invariant/optimistic lock/constraint violation | 409 | false (lock-only는 true) |
|
|
| RATE_LIMIT | rate limit/quota 초과 | 429 | true (Retry-After 이후 — D13) |
|
|
| TRANSIENT_DEPENDENCY | 외부 의존성 일시 실패 | 503 | true (Retry-After — D13) |
|
|
| PERMANENT_DEPENDENCY | 외부 의존성 영구 실패 | 502 | false |
|
|
| DATA_INTEGRITY | DB 무결성 위반 | 409 | false |
|
|
| INTERNAL | 분류 불가 내부 오류 | 500 | false |
|
|
|
|
### 2. MDC Key Standard (final)
|
|
|
|
> **Trace**: D11 (snake_case 강제). source / propagation channel 은 운영 wiring.
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: snake_case 선택 자체 (ECS dot notation / Micrometer dot.case 대안 존재). 표현 계층별 매핑은 §3 (D19).
|
|
> - **OUT_OF_BRANCH_SCOPE (Q12)**: `user_principal` 의 "pseudonymized" *알고리즘* (HMAC-SHA256 + `PSEUDONYMIZATION_SALT` 등)은 본 branch 범위 밖 — [[raw/branch-notes/feature-security-operational-baseline]] / [[raw/branch-notes/feature-log-management-contract]] owner. 본 branch 는 *log only + pseudonymized 형태로만 기록* 이라는 계약만 정의(평문 principal/raw id 금지).
|
|
|
|
snake_case 강제. MDC key 단위는 camelCase / dot.case 금지 (envelope/header 표현은 §3 매핑).
|
|
|
|
| MDC key | source | propagation channel |
|
|
|---------|--------|---------------------|
|
|
| request_id | inbound filter (생성 또는 X-Request-Id 헤더 — D14 sanitization / D15 검증 후) | response header X-Request-Id |
|
|
| trace_id | Micrometer Tracing | W3C traceparent header |
|
|
| span_id | Micrometer Tracing | W3C traceparent |
|
|
| correlation_id | inbound header X-Correlation-Id 또는 생성 (D14/D15) | HTTP X-Correlation-Id, message header correlation_id |
|
|
| tenant_id | tenant context (활성 시 — tenant-context-policy 도착 시) | downstream HTTP X-Tenant-Id (with allowlist) |
|
|
| user_principal | security context (pseudonymized only) | log only, headers forbidden |
|
|
|
|
### 3. ID 명명 표현 계층 매핑 (snake ↔ camel ↔ kebab)
|
|
|
|
> **Trace**: D19 + 부모 §21 (registry 매핑) + §25 (envelope camelCase owner = schema-serialization).
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: 케이스 선택 자체는 registry + schema-serialization 분담. 본 표는 *동일 식별자* 의 계층별 표현이 의도적 매핑임을 명문화 (branch 단독 독해 시 모순 오인 방지).
|
|
|
|
| 식별자 | MDC key (log) | envelope meta (JSON) | HTTP header |
|
|
|---|---|---|---|
|
|
| request id | `request_id` (snake) | `meta.requestId` (camel) | `X-Request-Id` (kebab) |
|
|
| trace id | `trace_id` (snake) | `meta.traceId` (camel) | `traceparent` (W3C lowercase) |
|
|
| span id | `span_id` (snake) | (envelope 미노출) | `traceparent` (W3C lowercase) |
|
|
| correlation id | `correlation_id` (snake) | `meta.correlationId` (camel) | `X-Correlation-Id` (kebab) |
|
|
| tenant id | `tenant_id` (snake) | (활성 시) | `X-Tenant-Id` (kebab) |
|
|
|
|
- **계약**: 같은 논리 식별자는 위 3-열이 1:1 매핑이어야 함. 표현 case 가 달라도 *의미* 는 동일 (테스트 계약 "response meta 의 ID 의미가 log MDC key 의미와 다르면 실패").
|
|
- **downstream 구속**: log-management-contract 가 user/resource id 의 MDC key 를 추가할 때 snake_case(`user_id`/`resource_id`) 사용 — resource-identifier §9 의 `user.id`/`resource.id`(dot) illustration 은 본 표준에 conform.
|
|
|
|
### 4. `error.details` JSON shape (validation field error)
|
|
|
|
> **Trace**: D12 + `JSONAPI-ERR-C3` (field pointer) + `GOOG-ERR-C5` (typed payload).
|
|
>
|
|
> - **`field` 출력 format (Q5, 구현 명확화)**: producer 는 한 응답에서 **dot path 를 default** 로 emit (Spring `FieldError.getField()` 가 native dot path — 변환 비용 0). JSON pointer(RFC 6901)는 nested/array 위치 표현이 필요한 경우에만 허용. 한 응답 내 혼용 금지.
|
|
> - **`rejectedValue` masking trigger (Q6, 구현 명확화)**: 민감 필드는 **(a) `@Sensitive`/`@Masked` 마커 annotation, 또는 (b) name denylist (`password`, `token`, `secret`, `apiKey`, `ssn`, `card*`) 매칭 시 `rejectedValue` 를 omit** (또는 `****`). default = omit. 정밀 DLP/PII 분류는 [[raw/branch-notes/feature-log-management-contract]] / [[raw/branch-notes/feature-security-operational-baseline]] owner.
|
|
> - **UNSUPPORTED_IMPL_DECISION**: dot-path default 선택 + denylist 어휘 자체는 ca-tmpl 운영 trade-off — 외부 표준 단일 근거 없음. validation 외 category 의 details 는 `null` (boundary `BATCH_PARTIAL_FAILURE` 만 별도 shape).
|
|
|
|
```json
|
|
{
|
|
"field": "user.email",
|
|
"rejectedValue": "<omitted if sensitive>",
|
|
"code": "VALIDATION_EMAIL_FORMAT",
|
|
"message": "invalid email format"
|
|
}
|
|
```
|
|
|
|
### 5. Retryable → `Retry-After` / `X-RateLimit-*` surfacing (D13)
|
|
|
|
> **Trace**: D13 + `RFC9110-C21` (Retry-After 503/3xx) + `RFC9110-C6` (413 temporary).
|
|
>
|
|
> - **format + 값 출처 (Q7, 구현 명확화)**: `Retry-After` 는 **delta-seconds(정수)** 를 default 로 emit (HTTP-date 아님 — skeleton 단순성). 값 = error-codes.yaml `retry_after_seconds` 컬럼. upstream 503 의 `Retry-After` passthrough 여부는 [[raw/branch-notes/feature-rate-limit-idempotency-contract]] / outbound adapter owner.
|
|
> - **UNSUPPORTED_IMPL_DECISION**: `retry_after_seconds` 값 + delta-seconds 선택 = error-codes.yaml / ca-tmpl project-decision. `X-RateLimit-*` 는 비표준 관례 (headers.yaml). 429 세부 = rate-limit-idempotency owner.
|
|
|
|
| category | HTTP | retryable | retry hint header |
|
|
|---|---|---|---|
|
|
| TRANSIENT_DEPENDENCY | 503 | true | `Retry-After` (MUST, RFC9110-C21) |
|
|
| RATE_LIMIT | 429 | true | `Retry-After` (+ `X-RateLimit-Limit/Remaining/Reset` 권고 — rate-limit owner) |
|
|
| 그 외 retryable=false | — | false | (헤더 없음) |
|
|
|
|
- envelope `error.retryable: true` 와 `Retry-After` 헤더는 **함께** 존재해야 함 (envelope 만 true + 헤더 부재 = 실패).
|
|
|
|
### 6. inbound 헤더 sanitization (D14) + trace trust boundary (D15)
|
|
|
|
> **Trace**: D14 + `OWASP-LOG-C1/C3/C5` (untrusted / CRLF sanitize / CWE-117); D15 + `W3C-TC-C2/C5` (traceparent format / propagation).
|
|
>
|
|
> - **strip vs encode + "구분자" 범위 (Q8, 구현 명확화)**: 본 skeleton 은 **구조화 JSON 로깅 전제** → 핵심 위협은 CR/LF/제어문자에 의한 *줄 위조*. default = **strip (제거)** of `\r` `\n` + ASCII 제어문자 (`< 0x20`). "구분자(delimiter) strip" 은 *pattern-layout 로깅을 쓸 때만* 해당 (그 경우 layout 구분자 추가 strip) — JSON 로깅에서는 불필요. reject(요청 거부)·encode 아님 (값은 보존하되 control char 만 제거).
|
|
> - **traceparent 무효 처리 위치 (Q9, 위임)**: 무효 `traceparent` → 새 trace 시작은 **Micrometer Tracing 의 W3C propagator 동작에 위임** (대부분 자동) — 본 branch 는 *수용/무효→재생성* 계약만 명시. propagator 구성/검증 detail = [[raw/branch-notes/feature-distributed-tracing-contract]] owner.
|
|
> - **UNSUPPORTED_IMPL_DECISION**: ①sanitization regex/charset/최대 길이 값 (OWASP 미규정 — 사용자 trade-off). ②trust 의 보안 세부 = security-operational-baseline owner (cross-cite).
|
|
|
|
- inbound `X-Request-Id` / `X-Correlation-Id` 수신 → MDC/로그 반영 전 **CR/LF/제어문자 strip** + **length cap** (값 부재/무효 시 server 생성). 위치 = `RequestLoggingFilter` (§0).
|
|
- inbound `traceparent` 수신 → W3C 4-field format(`W3C-TC-C2`) 검증. 무효 형식이면 **새 trace 시작** (client 값 무시 — Micrometer propagator 위임). 유효하면 propagation 의무(`W3C-TC-C5`).
|
|
- `tracestate` 에 PII 금지(`W3C-TC-C5` MUST NOT) — outbound 전파 시 동일.
|
|
|
|
### 7. operational error → trace span 기록 (D16, server-side)
|
|
|
|
> **Trace**: D16 + `OTEL-EXC-C1/C2/C4/C6` (exception event / attributes / span status ERROR / app-set status).
|
|
>
|
|
> - **대상 범위 (Q10, 구현 명확화)**: span status ERROR + exception 기록 대상은 **모든 5xx** — `INTERNAL`(500) + `PERMANENT_DEPENDENCY`(502) + `TRANSIENT_DEPENDENCY`(503). **4xx(client error)는 span status = `unset`** (OK 아님 — server-side fault 가 아니므로 ERROR 도 아님; OTel 기본 unset 유지). D16 텍스트의 "INTERNAL" 은 대표 예시이며 5xx 전체에 적용.
|
|
> - **UNSUPPORTED_IMPL_DECISION (4xx=unset 근거)**: `OTEL-EXC-C4` 는 "오류 시 SHOULD ERROR" 만 규정하고 *HTTP 4xx↔span status 매핑*은 직접 규정하지 않음(HTTP semconv 별도, 본 raw 미보관). "4xx=unset" 은 ca-tmpl 운영 trade-off — 실제 4xx/5xx↔span status wiring 은 [[raw/branch-notes/feature-distributed-tracing-contract]] owner 가 HTTP semconv 기준으로 확정.
|
|
> - **기록 위치 (Q11, 구현 명확화)**: `recordException` + `setStatus(ERROR)` 호출 위치 = `GlobalExceptionHandler`(§0) 또는 Micrometer Observation 의 error stop. 둘 중 택1 — skeleton default = Observation 자동(handler 가 Observation scope 안에서 던지면 자동 기록). 정확한 wiring = distributed-tracing owner.
|
|
> - **UNSUPPORTED_IMPL_DECISION**: client 응답 stack 제외는 OTel 직접 근거 없음 — ca-tmpl 보안 정책(D5/Forbidden). HTTP 5xx↔span ERROR 매핑은 HTTP semconv 별도. span detail 조립 = distributed-tracing owner.
|
|
|
|
- **모든 5xx** 발생 시 server span 에 `exception` 이벤트(`exception.type`/`exception.message`/`exception.stacktrace`) 기록 + span `status=ERROR`. 4xx 는 대상 아님.
|
|
- **client HTTP 응답에는 stack trace 미포함** (판정 기준 Forbidden 과 정합) — exception 세부는 *telemetry 전용*.
|
|
|
|
### 8. error code lifecycle (never-reuse, D17)
|
|
|
|
> **Trace**: D17 + `STRIPE-C2` (format/message 변경 = backward-compat → code 가 안정 표면) + `GOOG-ERR-C4` (machine-readable 식별자).
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: never-reuse 자체는 ca-tmpl 운영 정책 (resource-identifier D15 대칭). deprecation 절차(Deprecation/Sunset) = api-compatibility owner.
|
|
|
|
- `error.code` 는 **append-only** — rename / 의미 변경 / 재사용 금지. 폐기는 삭제가 아니라 deprecated 표시 + compatibility 절차.
|
|
- error-codes.yaml row 변경 시 `compatibility_impact` 컬럼(부모 §21 L812) 기반 registry-governance 검사.
|
|
- **deprecated code 의 yaml 표현 (Q13, UNSUPPORTED_IMPL_DECISION + 경계)**: 코드 ground truth — `error-codes.yaml` 은 현재 `compatibility_impact: none|additive|behavior-change|breaking` 컬럼만 있고 *deprecated/sunset 전용 컬럼은 미정의*. 폐기 표현 **제안 스케치**(미구현·미합의): row 에 `deprecated: true` + `sunset_date: YYYY-MM-DD` + `replacement_code:` 추가. 컬럼명·발행 시점·Deprecation/Sunset 헤더 연동의 *실제 절차*는 본 branch 범위 밖 — [[raw/branch-notes/feature-api-compatibility-deprecation-contract]] owner 가 확정. 본 branch 는 "code 는 폐기돼도 재사용 안 됨" 계약만 소유.
|
|
|
|
### 9. 401 → `WWW-Authenticate` (D18, cross-cite)
|
|
|
|
> **Trace**: D18 — 부모 §25 L1086 (security-operational-baseline owner) + RFC 9110 §11.6.1 MUST.
|
|
|
|
- `AUTH`(401) 응답은 `WWW-Authenticate` 헤더 동반 (bare 401 금지). 헤더 *발행 정책* 은 security-operational-baseline owner — 본 branch 는 enum 의 401 row 가 헤더 의무를 암시함을 명시만.
|
|
|
|
## 판정 기준
|
|
|
|
| 구분 | 기준 |
|
|
| --- | --- |
|
|
| Decision | 자체 structured envelope를 사용하고 `ProblemDetail`은 사용하지 않음 |
|
|
| Allowed | validation field error처럼 클라이언트가 수정 가능한 정보만 `error.details`에 포함 |
|
|
| Forbidden | exception class name, stack trace, SQL, token, internal endpoint, upstream raw body 노출 (client 응답). inbound 헤더 값의 미-sanitized 로그 반영 (D14) |
|
|
| Required fields | `success`, `error.code`, `error.category`, `error.retryable`, `meta.requestId`, `meta.traceId`, `meta.correlationId` |
|
|
| Required headers | retryable 응답(503/429)에 `Retry-After` (D13); 401 에 `WWW-Authenticate` (D18, security owner) |
|
|
| Failure condition | 5xx/validation/auth/access denied/no handler/type mismatch가 envelope와 log contract를 깨면 실패 |
|
|
|
|
## SSOT Ownership
|
|
|
|
| contract | owner decision | consumers |
|
|
| --- | --- | --- |
|
|
| error envelope schema | 이 branch에서만 field 추가/삭제/required 여부 변경 | business validation, schema serialization, API compatibility |
|
|
| `error.category` enum | 이 branch registry가 final (부모 §6 은 본 enum 으로 정합 — F1) | persistence, outbound, security, cache, message branches |
|
|
| request/correlation/trace ID meaning + 표현 매핑 (D19) | 이 branch 정의가 final (envelope camelCase 표기 owner = schema-serialization) | distributed tracing, log management, metrics alerting |
|
|
| MDC/log key names | 이 branch registry와 contract-registry branch가 final | log management, operational runbook |
|
|
| error code lifecycle (never-reuse, D17) | 이 branch + error-codes.yaml registry | api-compatibility (deprecation 절차) |
|
|
|
|
consumer branch가 위 값을 바꾸려면 이 branch의 Decision과 registry를 먼저 변경합니다.
|
|
|
|
## 테스트 계약
|
|
|
|
- 모든 실패 응답은 envelope schema를 만족해야 함.
|
|
- 5xx 응답에 raw exception class/stack trace가 client 응답에 노출되면 실패.
|
|
- requestId/traceId/correlationId가 response meta와 log MDC에 존재해야 함.
|
|
- tracing disabled profile에서도 `meta.traceId`가 비어 있거나 누락되면 실패.
|
|
- response meta의 ID 의미가 log MDC key 의미와 다르면 실패 (D19 매핑 위반).
|
|
- retryable 필드가 없는 operational error는 실패.
|
|
- retryable=true(503/429) 응답에 `Retry-After` 헤더가 없으면 실패 (D13).
|
|
- inbound 헤더(`X-Request-Id`/`X-Correlation-Id`) 값이 CR/LF sanitization 없이 로그에 기록되면 실패 (D14).
|
|
- 무효 형식 `traceparent` 수신 시 새 trace 를 시작하지 않고 그대로 채택하면 실패 (D15).
|
|
- `INTERNAL`(5xx) 발생 시 server span status 가 ERROR 로 설정되지 않으면 실패 (D16, server-side telemetry).
|
|
- `error.code` 의 rename/재사용이 compatibility 검사 없이 통과되면 실패 (D17, registry-governance).
|
|
- `ProblemDetail` 타입이나 필드 구조에 의존하는 테스트가 있으면 실패.
|
|
- MDC key 이름이 위 "MDC Key Standard" 표와 불일치하면 실패.
|
|
- `error.category` 값이 위 enum (`VALIDATION/AUTH/AUTHZ/NOT_FOUND/CONFLICT/RATE_LIMIT/TRANSIENT_DEPENDENCY/PERMANENT_DEPENDENCY/DATA_INTEGRITY/INTERNAL`) 외 값이면 실패.
|
|
- validation error response의 `error.details` 항목이 위 JSON shape(`field/rejectedValue/code/message`)를 따르지 않으면 실패.
|
|
|
|
## 엣지·실패·의존
|
|
|
|
> R4(깊이 게이트) 캡처용. 정상 경로 외에 *구현 중 부딪힐* 실패/엣지/다른 계약 의존을 미리 열거. (§테스트 계약·§구현 가이드 Q-notes·§형제 branch cross-cite·§SSOT Ownership 에 분산된 것을 R4 형식으로 통합 — 새 결정 없음.)
|
|
|
|
- **실패·엣지 경로** (각 경로의 기대 동작 — 위반 시 §테스트 계약 실패):
|
|
- **tracing disabled (local/test profile)** — `meta.traceId` 누락 금지. 기대: 같은 request 의 envelope `meta.traceId` == log MDC `trace_id`. **구현 위치·메커니즘 (actually-implemented)**: `RequestLoggingFilter` 가 micrometer-tracing 미공급 시 `trace_id` 를 `request_id` 로 미러(`RequestLoggingFilter.java` L52-54 주석 "trace_id mirrors request_id until micrometer-tracing supplies one (D7)"); `request_id` 자체는 inbound 헤더 부재/blank 시 서버 `UUID.randomUUID()` 생성(L75). `ResponseMetaFactory` 는 MDC 값을 *읽기만* 하므로 fallback 책임은 필터에 있음. (D7) — `trace.sampled=false` diagnostic 플래그 분리는 `planned`(distributed-tracing owner).
|
|
- **무효 형식 `traceparent` 수신** — 그대로 채택 금지. W3C 4-field 검증 실패 시 *새 trace 시작* (Micrometer propagator 위임). 과대 길이 헤더는 length cap. (D15 / `W3C-TC-C2`)
|
|
- **inbound 헤더 CR/LF 주입** (`X-Request-Id`/`X-Correlation-Id`) — MDC/로그 반영 전 `\r`/`\n`/제어문자(`<0x20`) strip. 기대: 주입 시도해도 로그 라인 1개 유지 + control char 부재. (D14 / `OWASP-LOG-C3/C5` / CWE-117)
|
|
- **5xx vs 4xx span 처리** — 모든 5xx(`INTERNAL`/`PERMANENT_DEPENDENCY`/`TRANSIENT_DEPENDENCY`)는 server span `status=ERROR` + `exception` 이벤트. **4xx 는 span ERROR 아님**(unset/OK). client 응답엔 stack 미포함. (D16 Q10/Q11)
|
|
- **retryable=true + `Retry-After` 부재** — envelope `error.retryable: true`(503/429)인데 `Retry-After` 헤더 없으면 실패. 둘은 *함께* 존재해야 함. (D13)
|
|
- **민감 필드 `rejectedValue`** — validation field 가 `password`/`token`/`secret` 등(annotation 또는 denylist 매칭)이면 `rejectedValue` omit/mask. default = omit. (D12 Q6)
|
|
- **enum/lifecycle 위반** — `error.category` 가 10-enum 외 값이거나, `error.code` 가 compatibility 검사 없이 rename/재사용되면 실패(append-only). (D10 / D17)
|
|
- **partial failure** — boundary `BATCH_PARTIAL_FAILURE` 는 본 branch `VALIDATION` category 의 consumer 이며 별도 details shape. validation 외 category 의 details 는 `null`. (boundary D5 공유)
|
|
|
|
- **다른 계약 의존** (해당 계약이 바뀌면 본 branch 영향):
|
|
- [[raw/branch-notes/feature-security-operational-baseline]] 의 `D18` 에 의존 — `WWW-Authenticate` 헤더 *발행 정책* owner. 본 branch 는 401 enum row 가 헤더 의무를 암시함만 명시(bare 401 금지). 발행 방식 변경 시 §9 cross-cite 갱신 필요.
|
|
- [[raw/branch-notes/feature-rate-limit-idempotency-contract]] 의 `D13`(429 세부) 에 의존 — 429 `Retry-After`(RFC 6585 §4, raw 미보관) + `X-RateLimit-*` 운영 세부 owner. 본 branch 는 retryable surfacing 계약만.
|
|
- [[raw/branch-notes/feature-distributed-tracing-contract]] 의 `D15`/`D16` 에 의존 — W3C propagator 구성/검증 + span 조립 detail owner. 본 branch 는 ID 의미 + error→span 기록 *의도* 만 정의.
|
|
- [[raw/branch-notes/feature-schema-serialization-contract]] — envelope `meta.*` camelCase 표기 owner. D19 의 snake↔camel 매핑은 이 owner 의 직렬화 규칙에 의존.
|
|
- [[raw/branch-notes/feature-log-management-contract]] — 본 branch MDC snake_case 표준을 consume(`user_id`/`resource_id` 추가 + redaction/PII MDC 분리). `user_principal` pseudonymization *알고리즘* 도 이 owner(§2 Q12 OUT_OF_BRANCH_SCOPE).
|
|
- [[raw/branch-notes/feature-resource-identifier-contract]] — MDC snake_case 정합(`user.id`/`resource.id` dot illustration → `user_id`/`resource_id` conform) + error code never-reuse(D17)와 ID never-reuse 대칭.
|
|
- [[raw/branch-notes/feature-api-compatibility-deprecation-contract]] — `error.code` deprecation/Sunset 절차 owner. lifecycle(D17) 폐기 흐름이 이 계약에 의존.
|
|
- [[raw/branch-notes/feature-contract-registry-governance]] — `error-codes.yaml`/`mdc-keys.yaml`/`metrics.yaml` row 편집 + diff gate owner. 본 branch 는 schema/category 매핑만, 실 row 는 registry.
|
|
|
|
## 검증해야 할 주장
|
|
|
|
| Claim | Why uncertain | How to verify | Status |
|
|
|---|---|---|---|
|
|
| Spring Boot `spring.mvc.problemdetails.enabled` 가 default `false` (ca-tmpl 의 ProblemDetail 거부 정책과 충돌 없음) | `SPRING-PD-C4` Does not prove: property default 값 본 인용 범위 밖 | Spring Boot reference docs 별도 fetch + `application.yml` 검증 | `planned` |
|
|
| ca-tmpl envelope 의 `meta.traceId` 가 모든 5xx/validation/auth 응답에서 누락 없이 채워짐 | foundation owner branch — 모든 ControllerAdvice/Filter 에서 동작 보장 필요 | contract test: 각 카테고리별 fixture exception 발생 → response body 의 `meta.traceId` non-empty 확인 | `planned` |
|
|
| ProblemDetail 타입/필드 의존 테스트 부재 (build-time 강제) | Spring Boot 가 일부 built-in exception 을 ProblemDetail 로 자동 변환 (`SPRING-PD-C2`/`C3`) — autoconfigure 누락 시 leak 가능 | ArchUnit test: `org.springframework.http.ProblemDetail` import 금지 + `application.yml` 의 `spring.mvc.problemdetails.enabled=false` 확인 | `planned` |
|
|
| `error.category` enum 10개 의 retryable default (RATE_LIMIT/TRANSIENT_DEPENDENCY = true, 나머지 false) 가 실제 운영에서 정합 | enum default 는 ca-tmpl 운영 가정 — 일부 (e.g., NOT_FOUND with eventual consistency) 는 retryable 일 수 있음 | 실제 incident 회고 + adapter 별 retryable override 메커니즘 검증 | `needs-confirmation` |
|
|
| MDC key snake_case (`request_id`) 가 Spring MVC `RequestContextHolder` 와 Reactor Context 양쪽에서 일관 propagation | foundation 결정 — 실제 reactive stack 에서 MDC 전파 확인 필요 | reactive integration test + `@Async` test | `planned` |
|
|
| tracing disabled profile (e.g., local) 에서 request-id fallback traceId 의 uniqueness + log-envelope 정합 | 현재 fallback은 `RequestLoggingFilter`가 서버 생성 `request_id`를 `trace_id`로 미러링하며 `ResponseMetaFactory`는 MDC 값을 읽는다. 별도 `trace.sampled=false` 표시는 distributed-tracing owner에 남아 있음 | local profile 통합 테스트 — 같은 request 의 envelope traceId == log traceId 확인 | fallback 메커니즘 `actually-implemented`; 통합 테스트와 sampled flag는 `planned` |
|
|
| `error.details` 의 `rejectedValue` 가 PII/sensitive 값 일 때 자동 masking (e.g., password field) | validation field 가 password 일 때 rejectedValue 그대로 노출 위험 | contract test: password field validation 실패 → rejectedValue 가 `****` 또는 omitted | `planned` |
|
|
| `error.category=DATA_INTEGRITY` (default 409) 와 ca-tmpl 의 DB optimistic lock (CONFLICT, retryable=true) 분기 정합 | DATA_INTEGRITY vs CONFLICT 모두 409 — runtime 분류 logic 명확성 필요 | persistence adapter exception → category 매핑 contract test | `planned` |
|
|
| (D13) retryable=true(503/429) 응답에 `Retry-After` 헤더가 envelope `error.retryable` 와 함께 존재 | RFC9110-C21 은 503/3xx 만 normative — 429 는 RFC 6585; 헤더 발행이 실제 wiring 됐는지 미검증 | contract test: 503/429 fixture → 응답 헤더 `Retry-After` non-empty + envelope retryable=true | `planned` |
|
|
| (D14) inbound `X-Request-Id` 에 `\r\n` 주입 시 로그가 1줄로 유지되고 CR/LF 가 strip | OWASP 는 원칙만 — 실제 sanitizer 구현/적용 위치 미검증 | injection test: `X-Request-Id: foo\r\nFAKE LOG` → 로그 라인 1개 + control char 부재 grep | `planned` |
|
|
| (D15) 무효 형식 `traceparent` 수신 시 새 trace 시작 + 과대 헤더 length cap | W3C 무효 처리/길이 한계 raw 미보관 — 구현 분기 미검증 | integration test: malformed `traceparent` → 신규 trace-id 생성; 초과 길이 헤더 거부/절단 | `planned` |
|
|
| (D16) 5xx 발생 시 server span status=ERROR + `exception` 이벤트 기록, client 응답엔 stack 부재 | OTel 사양은 attribute 정의만 — 실제 SDK wiring + 응답 분리 미검증 | OTel in-memory test exporter: span status ERROR + exception event 존재; 동시에 response body 에 stacktrace 부재 grep | `planned` |
|
|
| (D17) `error.code` rename/삭제/재사용 시 compatibility 검사 실패 | never-reuse 는 정책 — registry-governance 자동 게이트 미검증 | error-codes.yaml diff gate (removed/renamed code 검출 시 build 실패) — registry-governance owner | `needs-confirmation` |
|
|
| (D18) `AUTH`(401) 응답에 `WWW-Authenticate` 헤더 존재 | 헤더 발행 owner = security-operational-baseline — cross-branch 정합 미검증 | security-baseline contract test (401 → `WWW-Authenticate` non-empty) cross-link | `planned` |
|
|
| (D19) 동일 식별자의 MDC snake ↔ envelope camel ↔ header kebab 매핑이 일관 | 표현 case 가 달라 매핑 drift 위험 | contract test: 한 request 의 `meta.requestId` == MDC `request_id` == `X-Request-Id` (값 동일) | `planned` |
|
|
| (D13/F1 검증) `error-codes.yaml` 의 `AUTH_KID_UNKNOWN` 이 `category=AUTH, retryable=false` 인데 `retry_after_seconds: 5` 보유 — D13("retryable 응답만 Retry-After surface")과 모순 | 2026-06-01 registry 검증에서 발견된 유일 이상치. source 주석(`feature-security-operational-baseline` L88 "JWKS 미캐시 → 401 + Retry-After 5s") + `client_safe_message: "please retry"` 가 retryable 의도를 시사 | **해소됨 (2026-06-01)**: 사용자 결정 = JWKS 키 회전 가정 → `retryable: true` 로 수정 (option b). `retry_after_seconds=5`/`runbook_link` 유지, §21 runbook 규칙 충족, yaml parse OK. **가역** — 키 고정 정책 전환 시 `false` 복귀(yaml inline 주석 명시) | `locally-verified` (registry 정합 확인; contract-verification:auth-category 테스트는 CI/사용자 실행) |
|
|
|
|
## Phase C2 구현 진행 현황 (완료 — 2026-06-01)
|
|
|
|
> **이력 정정**: 초기에 subagent 루프가 Task 1/2/2b 를 개별 커밋(`ca7e12f`/`5945d07`/`31ea05c`/`fad374f`)으로 진행했으나, 사용자 요청으로 `git reset --mixed` 하여 **그 커밋들은 폐기**(SHA 무효)하고 변경은 working tree 에 보존, 이후 나머지 Task 를 직접 편집으로 완료. **단일 커밋은 사용자가 직접 생성 예정** — 본 노트는 SHA 대신 *파일·검증* 기준으로 기록.
|
|
|
|
**상태**: G1·G2·G3·G4·G5·G7 = `actually-implemented` `locally-verified`. G6(Retry-After 헤더 발행 / span 기록) = seam·stub 만(owner branch 위임 — D21). 전체 검증: `./gradlew check` (전 모듈 test + `verifyCleanArchitectureDependencies` + ArchUnit `CleanArchitectureTest`) → **BUILD SUCCESSFUL**.
|
|
|
|
### 신규 파일
|
|
- `shared-contract`: `error/Category.java`(10-enum, G7/D10), `response/ResponseMeta.java`(requestId/traceId/correlationId, G2)
|
|
- `adapter-web/observability/`: `MdcKeys.java`(snake_case 상수), `HeaderSanitizer.java`(CR/LF·제어문자 strip — D14/CWE-117), `ResponseMetaFactory.java`(snake MDC→camel meta 투영 — D19), `RetryAfterAdvisor.java`(D13/D16 cross-owned seam — D21)
|
|
- 테스트: `CategoryTest`/`ApiErrorTest`/`EnvelopeTest`(shared-contract), `HeaderSanitizerTest`/`ResponseMetaFactoryTest`/`RetryAfterAdvisorTest`/`RequestLoggingFilterTest`/`EnvelopeMetaIntegrationTest`(adapter-web), `PortfolioErrorCodeTest`(sample-portfolio)
|
|
|
|
### 수정 파일
|
|
- `shared-contract`: `ApiErrorCode.category()` 추가, `OperationalError` 코드별 category 매핑(retryable per-code 유지), `ApiError`에 category 필드, `Envelope`/`BulkEnvelope` flat `traceId`→`ResponseMeta meta`
|
|
- `adapter-web`: `ErrorResponseFactory`/`EnvelopeBodyAdvice`/`HealthcheckController`(meta+category 반영), `RequestLoggingFilter`(snake_case MDC + X-Correlation-Id + sanitization)
|
|
- `app-bootstrap`: `logback-spring.xml`(snake_case includeMdcKeyName), `application.yml`/`application-test.yml`(`spring.mvc.problemdetails.enabled=false`)
|
|
- `sample-portfolio`: `PortfolioErrorCode`(category()), `WorkLogController`(ResponseMetaFactory), `BulkEnvelopeTest`/`VirtualThreadMdc*Test`(ResponseMeta·snake_case 정합)
|
|
|
|
### category 할당표 (OperationalError)
|
|
`VALIDATION_FAILED/BAD_PARAMETER/MAPPING_FAILED/BATCH_PARTIAL_FAILURE/METHOD_NOT_ALLOWED/UNSUPPORTED_MEDIA_TYPE → VALIDATION`, `INTERNAL_ERROR → INTERNAL`, `UNAUTHENTICATED/INVALID_TOKEN → AUTH`, `FORBIDDEN → AUTHZ`, `ROUTE_NOT_FOUND → NOT_FOUND`. (405/415→VALIDATION 은 UNSUPPORTED_IMPL_DECISION — 10-enum 에 transport 카테고리 없음.) `retryable` 은 per-code 유지(§1 Q1 — `INTERNAL_ERROR` 처럼 category default 와 다른 코드 허용).
|
|
|
|
## 마주친 문제
|
|
|
|
- **(해소) `@WebMvcTest` nested `@SpringBootConfiguration` 컨텍스트 오염**: 신규 envelope-meta 계약 테스트를 `app-bootstrap` 의 `@WebMvcTest`(nested `@SpringBootConfiguration` 포함)로 작성했더니, (a) production profile placeholder 로 `BindException`, (b) 같은 패키지 `OperationalContractRuntimeTest` 의 config 자동 탐지를 오염시켜 `EnvelopeBodyAdvice` 미등록 회귀. git stash / 파일 mv 비파괴 격리로 원인 확정 → 세 컴포넌트가 모두 adapter-web 소속이므로 **adapter-web standalone MockMvc(`EnvelopeMetaIntegrationTest`)로 재설계**해 해소. 상세: [[raw/errors/webmvctest-nested-springbootconfiguration-context-pollution-2026-06-01]]
|
|
- **(해소) 인터페이스 변경의 숨은 consumer 컴파일 break**: `ApiErrorCode.category()` 추가 → `PortfolioErrorCode`(구현체), `ApiError`/`Envelope`/`BulkEnvelope` 시그니처 변경 → `BulkEnvelopeTest`(`allOk(List,String)`/`traceId()`) 컴파일 실패. 컴파일러가 전부 노출 → 한 패스 마이그레이션. (계획 누락 consumer 였음 — 추상 메서드 추가의 blast radius 가 *안전장치*로 작동.)
|
|
|
|
## 묶음 (이 branch에서 파생된 자료)
|
|
|
|
<!-- GENERATED: sources:start -->
|
|
- [[raw/company-tech-blogs/github-api-error-format]]
|
|
- [[raw/company-tech-blogs/stripe-error-format]]
|
|
- [[raw/company-tech-blogs/toss-payments-error-format]]
|
|
- [[raw/official-docs/google-api-error-format]]
|
|
- [[raw/official-docs/graphql-errors-spec]]
|
|
- [[raw/official-docs/json-api-errors-spec]]
|
|
- [[raw/official-docs/otel-exceptions-semantic-conventions]]
|
|
- [[raw/official-docs/owasp-logging-cheat-sheet]]
|
|
- [[raw/official-docs/persistence-spring-dataaccessexception-hierarchy]]
|
|
- [[raw/official-docs/problem-detail-rfc-7807]]
|
|
- [[raw/official-docs/spring-problem-detail]]
|
|
<!-- GENERATED: sources:end -->
|
|
|
|
<!-- GENERATED: interviews:start -->
|
|
- [[raw/interviews/operational-error-envelope-and-observability-foundation]]
|
|
<!-- GENERATED: interviews:end -->
|
|
|
|
<!-- GENERATED: errors:start -->
|
|
- [[raw/errors/webmvctest-nested-springbootconfiguration-context-pollution-2026-06-01]]
|
|
<!-- GENERATED: errors:end -->
|
|
|
|
<!-- GENERATED: blog-topics:start -->
|
|
- [[raw/blog-topics/operational-error-envelope-meta-category-migration-2026-06-01]]
|
|
<!-- GENERATED: blog-topics:end -->
|
|
|
|
> 본 feature branch 는 leaf — 자식 branch 없음. Phase C2 실 코드 작성 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.
|
|
|
|
### Sub-branches (세부 작업)
|
|
|
|
- (없음 — project 직접 자식 branch, 하위 branch 없음)
|
|
|
|
### 근거 자료
|
|
|
|
- [[raw/official-docs/rfc9110-http-semantics]] — D13: `Retry-After` semantics (RFC9110-C21/C6). D18: 401+WWW-Authenticate §11.6.1 cross-cite
|
|
- [[raw/official-docs/tracing-w3c-trace-context-spec]] — D15: `traceparent` 4-field 형식 + propagation/PII 의무 (W3C-TC-C2/C5)
|
|
- [[raw/official-docs/owasp-logging-cheat-sheet]] — D14: inbound header MDC 값 sanitization / log injection(CWE-117) 방어 (OWASP-LOG-C1/C3/C5)
|
|
- [[raw/official-docs/otel-exceptions-semantic-conventions]] — D16: span exception 이벤트 + span status ERROR 공식 사양 (OTEL-EXC-C1/C2/C4/C6)
|
|
- [[raw/official-docs/stripe-resource-id-convention]] — D17: opaque string/error message 변경 = backward-compatible → code 안정성 (STRIPE-C2)
|
|
- [[raw/official-docs/google-api-error-format]] — D10/D12/D17: google.rpc.Code enum + ErrorInfo (GOOG-ERR-C1/C4/C5)
|
|
|
|
### 오류 기록 (본 feature 작업 중 발생)
|
|
|
|
- [[raw/errors/webmvctest-nested-springbootconfiguration-context-pollution-2026-06-01]] — Phase C2 envelope-meta 계약 테스트 추가 중 `@WebMvcTest` nested `@SpringBootConfiguration` 이 같은 패키지 `OperationalContractRuntimeTest` 컨텍스트를 오염시킨 회귀. 격리 진단 → standalone MockMvc 재설계로 해소.
|
|
|
|
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
|
|
|
- [[raw/interviews/operational-error-envelope-and-observability-foundation]] — 운영 envelope 에 category/meta 를 additive 로 확장, snake↔camel↔kebab 식별자 매핑, inbound 헤더 log injection 방어, category vs per-code retryable 분리.
|
|
|
|
### 강의 (이 작업을 위해 학습한 강의)
|
|
|
|
- (없음 — official-doc 근거 기반)
|
|
|
|
### job-posting tie-ins (이 작업에서 파생된 글감)
|
|
|
|
- [[raw/blog-topics/operational-error-envelope-meta-category-migration-2026-06-01]] — 운영 에러 분류 enum SSOT 고정 + envelope category/meta additive 마이그레이션 + 식별자 계층 매핑 + log injection 방어 + @WebMvcTest 오염 트러블슈팅(5개 글감 후보).
|
|
|
|
## 관련 일일 노트
|
|
|
|
- (없음 — scaffolding + reinforcement + Phase C2 구현 단계, 별도 일일 노트 미작성)
|
|
|
|
## 관심사 커버리지 (coverage-auditor 자동 생성)
|
|
|
|
> `/coverage` 가 생성하는 **생성물** — 손유지 금지. 기준: `rules/coverage-gate.md`. governing_docs: `api-error-envelope-design` + `observability-log-metric-trace-runbook`.
|
|
> 마지막 감사: 2026-06-04 → **Covered** (Blocking 0 / Should-fix 0 / Advisory 0). Should-fix(UNLINKED_DELEGATION)는 본 표 추가로 해소.
|
|
|
|
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
|
|--------|------|-------|--------|------|
|
|
| custom envelope 채택 + ProblemDetail 거부 (success/error 대칭) | covered-here | — | — | D1 (official-standard RFC7807 + official-vendor-doc Spring PD); `Envelope.java` actually-implemented |
|
|
| `error.category` 10-enum 1급 필드 | covered-here | — | — | D10 (official-vendor-doc Google AIP-193); `Category.java` 10값 actually-implemented |
|
|
| envelope fields `success/data/error/meta` + code UPPER_SNAKE_CASE + retryable 1급 | covered-here | — | — | D3 (UNSUPPORTED_DECISION — success flag 외부 표준 없음), D4 (company-case-study); `ApiError.java` actually-implemented |
|
|
| client-safe message vs internal diagnostic 분리 | covered-here | — | — | D5 (official-standard RFC7807-C5 + official-vendor-doc GOOG-ERR-C2); §판정기준 Forbidden |
|
|
| `error.details` `{field, rejectedValue, code, message}` | covered-here | — | — | D12 (official-standard JSON:API + official-vendor-doc Google AIP-193) |
|
|
| `meta.{requestId, traceId, correlationId}` envelope 1급 노출 | covered-here | — | — | D19·D20 (project-ssot §3/§21/§25); `ResponseMeta.java` actually-implemented |
|
|
| exception leak 금지 (stack/SQL/token/internal path 응답 미포함) | covered-here | — | — | D5 + §판정기준 Forbidden |
|
|
| envelope 대안 5종 비교·거부 근거 | covered-here | — | — | D1 + §외부 근거/대안 조사 |
|
|
| error code catalog → `error-codes.yaml` SSOT (본 branch 는 schema/category 매핑만) | covered-here | — | — | D9 (company-case-study GH-ERR-C4); `error-codes.yaml` ground-truth 확인 |
|
|
| `error.category` enum + envelope schema SSOT ownership | covered-here | — | — | D6 (UNSUPPORTED_DECISION — 내부 운영 정책); §SSOT Ownership |
|
|
| MDC key snake_case 표준 | covered-here | — | — | D11 (UNSUPPORTED_DECISION — 공식 표준 없음, ECS/Micrometer 대안); `MdcKeys.java` actually-implemented |
|
|
| ID 표현 계층 매핑 (MDC snake ↔ envelope camel ↔ HTTP kebab) | covered-here | — | — | D19 (project-ssot); `ResponseMetaFactory.java` actually-implemented |
|
|
| requestId/traceId/correlationId 의미 final 정의 | covered-here | — | — | D8 (official-standard W3C-TC-C2 for traceId; requestId/correlationId = UNSUPPORTED_DECISION) |
|
|
| traceId never-missing (tracing disabled 시 generated opaque id) | covered-here | — | — | D7 (UNSUPPORTED_DECISION — noop tracer 직접 근거 없음); `RequestLoggingFilter.java` actually-implemented |
|
|
| inbound 헤더 CR/LF sanitization (CWE-117) | covered-here | — | — | D14 (official-reference OWASP-LOG-C1/C3/C5); `HeaderSanitizer.java` actually-implemented |
|
|
| traceparent trust boundary (format 검증 + 무효 시 재생성) | covered-here | — | — | D15 (official-standard W3C-TC-C2/C5); propagation 세부 위임 → [[raw/branch-notes/feature-distributed-tracing-contract]] |
|
|
| 5xx span status ERROR + exception 이벤트 기록 의도 | covered-here | — | — | D16 (official-vendor-doc OTEL-EXC-C1/C2/C4/C6); span 조립 세부 위임 → [[raw/branch-notes/feature-distributed-tracing-contract]] |
|
|
| `Retry-After` 헤더 surface (503 MUST / 429 권고) | covered-here | — | — | D13 (official-standard RFC9110-C21/C6); 429 세부 위임 → [[raw/branch-notes/feature-rate-limit-idempotency-contract]] |
|
|
| `WWW-Authenticate` on 401 (RFC 9110 §11.6.1 MUST) | delegated | [[raw/branch-notes/feature-security-operational-baseline]] D18 | — | 발행 정책 owner; 본 branch §9 cross-cite (envelope/category 측만) |
|
|
| `error.code` lifecycle (append-only, never-reuse, deprecation 절차) | covered-here | — | — | D17 (company-case-study STRIPE-C2 + official-vendor-doc GOOG-ERR-C4; never-reuse = UNSUPPORTED_DECISION); 폐기 절차 위임 → [[raw/branch-notes/feature-api-compatibility-deprecation-contract]] |
|
|
| Phase C2 phasing 결정 (G1~G7 구현 범위 분리) | covered-here | — | — | D21 (UNSUPPORTED_DECISION — 사용자 결정 2026-06-01) |
|
|
| structured JSON Logback + masking/redaction + log sampling | delegated | [[raw/branch-notes/feature-log-management-contract]] | — | 본 branch MDC snake_case 표준 consume; PII MDC 분리 owner |
|
|
| Micrometer dot.case metric + Prometheus + alert severity + error_code cardinality bound | delegated | [[raw/branch-notes/feature-metrics-alerting-contract]] | — | 본 branch enum 을 metric tag dimension 으로 consume |
|
|
| W3C traceparent 전파 + sampling + OTel bridge wiring | delegated | [[raw/branch-notes/feature-distributed-tracing-contract]] | — | 본 branch 는 ID 의미 + error→span 기록 *의도* 만 정의 |
|
|
| envelope camelCase 직렬화 규칙 | delegated | [[raw/branch-notes/feature-schema-serialization-contract]] | — | D19 에 "envelope camelCase owner = schema-serialization" 명기 |
|
|
| error-codes/mdc-keys/metrics.yaml row 편집 + diff gate | delegated | [[raw/branch-notes/feature-contract-registry-governance]] | — | 본 branch 는 schema/category 매핑만, 실 row 는 registry |
|
|
|
|
> **STALE_OWNER 참고 (coverage 범위 밖, `/ingest` 선행 조건)**: governing doc `wiki/projects/ca-tmpl/api-error-envelope-design.md` (status `draft`, `documented-only` 태그)가 코드 실측(Phase C2 완료)보다 stale. 본 branch verified 추출 전 해당 canonical status 갱신 필요.
|
|
|
|
## 완료 후 정리
|
|
|
|
> **Ground-truth 대조 (2026-06-04, /ingest):** ca-tmpl @0c996fc("운영 에러 관측성 foundation 계약 구현")의 코드를 실측한 결과 G1~G5/G7(envelope `meta`/`category` 1급, `Category` 10-enum, MDC snake_case, `correlation_id` 처리, `HeaderSanitizer`)가 모두 코드에 존재하고 HEAD `db61075`에서도 유지됨. `Envelope`/`ApiError`/`ResponseMeta`/`Category`/`OperationalError`/`MdcKeys`/`HeaderSanitizer`/`ResponseMetaFactory`/`RequestLoggingFilter` 실재 확인. ProblemDetail 거부는 ArchUnit `CleanArchitectureTest`(L355) + `application.yml` `problemdetails.enabled:false` + `ProblemDetailDisabledConfigTest`로 build-time 강제. G6(Retry-After 헤더 발행/span ERROR)는 `RetryAfterAdvisor` seam/stub만(owner branch 위임). `tenant_id` MDC 키는 아직 미정의(조건부). stale 잔재(`com.example.blog`/`sample-ticket`) 없음 — sample 모듈 `sample-portfolio`. → branch `status: verified`. governing project docs 2건 + concept docs 2건 동기화 완료.
|
|
|
|
- PR 링크:
|
|
- 리뷰 메모:
|
|
- 머지 결과 / 배포 환경:
|
|
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
|
|
- `actually-implemented` 항목:
|
|
- `locally-verified` 항목:
|
|
- `prod-verified` 항목:
|
|
- **추출하지 않을 항목** (planned / documented-only / abandoned):
|