Files
llm-wiki/raw/branch-notes/feature-business-rule-validation-contract.md

374 lines
36 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: branch / feature-business-rule-validation-contract
source_type: branch-note
status: raw
branch: feature-business-rule-validation-contract
parent_branch:
related_projects: [ca-skeleton]
governing_docs: [wiki/projects/ca-tmpl/clean-architecture-package-layout, wiki/projects/ca-tmpl/api-error-envelope-design]
tags: [branch, ca-skeleton, validation, business-rule, domain]
created: 2026-05-22
target_merge:
status_label: in-progress
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-037
kind: project-work-item
project: ca-skeleton-operational-contract
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-037
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MAPPING-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-ERROR-ENVELOPE-001@1]
refines: []
overrides: []
depends_on: []
contract_packet: 1
contract_packet_sha256: 9fe64ae8128001379c77396ee11cfe7afe9196c837a5de4b2500c0c443b6213b
---
# branch: feature-business-rule-validation-contract
> Layer: `raw/branch-notes/` — syntax validation, use case policy, business invariant, persistence integrity 검증 책임을 분리합니다.
<!-- 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 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다.
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: validation ownership·mapper failure contract test가 통과한다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MAPPING-001@1` | 수기 mapper와 record canonical constructor가 default이며 MapStruct는 optional profile이다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
| `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 -->
## 목표
validation이라는 이름으로 모든 규칙이 controller DTO나 DB constraint에 몰리면 도메인 적용 후 유지보수가 무너집니다. 어떤 규칙을 어느 경계에서 검증할지 명확히 분리합니다.
- 이슈:
- PR:
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- request syntax/shape validation.
- application policy validation.
- domain invariant validation.
- persistence uniqueness/integrity handling.
- duplicate validation 허용 기준.
- validation error response/log 기준.
### 제외 범위
- 특정 비즈니스 규칙 설계.
- frontend validation 정책.
- database schema design 전체.
## TODO
> TODO drained 2026-05-22 — 결정은 아래 "결정 사항" / "Decisionized Work Items" / "판정 기준" 참조. syntax/policy/invariant/persistence/duplicate/validation details 모두 표 row로 반영됨. 잔존 TODO 없음.
## 진행 중 메모
> 작업하며 떠오른 메모. 자유 형식.
- 현재 documented-only 단계 — D1~D9 결정·근거 + §구현 가이드 명세 작성 완료, 실제 코드 미착수.
- D5/D6/D7 (envelope shape + code→category 매핑) 은 sibling `feature-boundary-validation-mapping-contract` 와 결정이 중첩 — 구현 명세는 sibling 소유로 정제(§Audit F1/F3). 본 branch 는 4-layer 책임 view 에 집중.
- **2026-06-02 ca-tmpl ground-truth 패스** (실 코드/registry 대조):
- **F5 RESOLVED** — `PERSISTENCE` enum 은 실재하지 않음(`Category.java` 10-enum). 실제 매핑 `DB_UNIQUE_VIOLATION`→CONFLICT / `DB_NULL·FK·CHECK`→DATA_INTEGRITY 로 전 표 정합.
- **persistence integrity 핸들러 미구현 확인** — `GlobalExceptionHandler``DataIntegrityViolationException` 핸들러 없음. owner `feature-persistence-failure-baseline`(documented-only). §2 에 `planned` 명시.
- **F2 보강** — policy → AUTHZ 실재 코드(`AUTHZ_INSUFFICIENT_PERMISSION`/`AUTHZ_TENANT_MISMATCH`) 확인, 단 owner 는 security/tenant branch → consume. D-ID gap 은 여전히 open.
- open gap (잔존): use case policy layer D-ID 미부여(§Audit F2). D1/D2 외부 근거 보강 deferred(§Audit F4). `error-codes.yaml:580` 주석의 stale `PERSISTENCE` 는 ca-tmpl 레포 측 정리 대상.
## Work Item Contract
각 TODO는 아래 판정 단위로 재작성되어야 canonical 승급 가능합니다. TODO가 단순히 `기준 작성`으로 남아 있으면 이 branch는 완료로 보지 않습니다.
| field | required | rule |
| --- | --- | --- |
| Decision | yes | 구현자가 선택해야 하는 기본값 |
| Allowed | yes | 허용되는 예외와 조건 |
| Forbidden | yes | 절대 금지되는 구현/문서 상태 |
| Required registry update | conditional | error/env/header/log/metric/capability 변경 시 필수 |
| Required contract test | yes | 계약 위반 시 실패해야 하는 테스트 |
| Failure condition | yes | review/build에서 실패로 판정할 상태 |
| Canonical extraction target | yes | `wiki/projects` 승급 위치 |
## 결정 사항
- 2026-05-22: request DTO validation은 입력 모양 검증만 담당.
- 2026-05-22: business invariant는 domain에서 검증.
- 2026-05-22: persistence integrity error는 operational error로 변환하되 client-safe message만 응답.
- 2026-05-22: 이 branch의 TODO도 Work Item Contract를 따라야 하며 아래 Decisionized Work Items가 canonical 승급 기준이다.
## 근거 (필수, 최소 1개+)
> 본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.
| Source | 정당화하는 결정 |
|---|---|
| [[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과 충돌 |
| [[raw/official-docs/google-api-error-format]] | 가장 표현력 풍부, retryable detail 1급 |
| [[raw/official-docs/json-api-errors-spec]] | — |
| [[raw/official-docs/graphql-errors-spec]] | partial success 1급 |
| [[raw/company-tech-blogs/github-api-error-format]] | — |
## 외부 근거 / 대안 조사 (2026-05-22 — Topic 4)
본 branch의 business invariant violation → CONFLICT/VALIDATION mapping 결정에 대한 외부 source 조사. error.category enum과 1:1.
- **채택 결정 (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급화.
## Decisionized Work Items
| field | Decision | Allowed | Forbidden | Required registry update | Required contract test | Failure condition |
|-------|----------|---------|-----------|----------------------------|-------------------------|---------------------|
| syntax/shape | request DTO validation | frontend duplicate validation | domain-only syntax validation | error-registry row 변경 시 VALIDATION 코드 추가 | malformed request test | malformed request가 VALIDATION envelope로 매핑되지 않으면 실패 |
| use case policy | application policy validation | domain service if pure domain rule | controller-only authorization policy | error-registry row 변경 시 AUTHZ/CONFLICT 코드 추가 | policy conflict test | policy violation이 AUTHZ/CONFLICT envelope로 매핑되지 않으면 실패 |
| domain invariant | domain model/value object | pre-check for UX/perf | DB constraint as only invariant | error-registry row 변경 시 CONFLICT/VALIDATION 코드 추가 | invariant test | invariant violation이 infrastructure exception으로 표현되면 실패 |
| persistence integrity | infrastructure maps to operational error | application pre-check | raw SQL/constraint in response | error-registry row 변경 시 DATA_INTEGRITY(null/FK/check) / CONFLICT(unique) 코드 추가 | integrity mapping | unique/integrity failure가 raw SQL/constraint name을 client에 노출하면 실패 |
| duplicate validation | allowed with canonical owner | documented redundancy | contradictory duplicate rules | 없음 (boundary 책임만) | boundary test | canonical owner 없는 duplicate rule이 추가되면 실패 |
| validation details | safe field errors only | no details for security | raw object/body/SQL detail | 없음 (error-registry envelope shape에 종속) | leakage test | raw object/body/SQL detail이 response에 노출되면 실패 |
## 판정 기준
| 구분 | 기준 |
| --- | --- |
| Decision | validation 책임을 boundary별로 분리 |
| Allowed | 같은 규칙을 UX/성능 목적으로 사전 검증하되 canonical owner를 명시 |
| Forbidden | DB constraint만으로 business invariant를 대체 |
| Required mapping | syntax -> VALIDATION, policy -> AUTHZ/CONFLICT, invariant -> CONFLICT/VALIDATION, persistence -> DATA_INTEGRITY(null/FK/check)/CONFLICT(unique) |
| Failure condition | raw persistence exception이나 domain exception이 presentation까지 새면 실패 |
## 테스트 계약
- malformed request는 structured validation error로 변환되어야 함.
- business invariant violation이 infrastructure exception으로 표현되면 실패.
- unique constraint failure가 SQL/constraint raw name을 클라이언트에 노출하면 실패.
- 이 branch의 TODO가 Decision/Allowed/Forbidden/Test 없이 남으면 canonical 승급 실패.
## 결정-근거 매핑
> 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. company-tech-blog 출처는 `company-case-study` 로 라벨링하며 공식 best practice 로 격상하지 않는다.
| Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|
| D1 | request DTO validation 은 입력 모양 (syntax/shape) 검증만 담당 (2026-05-22) | UNSUPPORTED_DECISION (4-layer validation 분리는 project-internal architectural decision; 외부 표준이 boundary 별 책임 분할을 normative 로 강제하지 않음) | N/A | layer 책임의 정합성은 sibling branch (`feature-boundary-validation-mapping-contract`) 와 cross-review 필수 — 동일 4-layer 결정이 양쪽에 분산됨 |
| D2 | business invariant 는 domain 에서 검증 | UNSUPPORTED_DECISION (DDD aggregate invariant 책임 패턴은 일반 design wisdom 이지만 본 branch 가 cite 한 sources — Stripe/Toss/RFC 7807/Spring/Google/JSON:API/GraphQL/GitHub — 중 normative 진술 없음) | N/A | DDD aggregate / value object 책임 패턴의 raw 인용 (예: Vaughn Vernon, Fowler anemic vs rich) 별도 보강 필요 |
| D3 | persistence integrity error 는 operational error 로 변환하되 client-safe message 만 응답 | `raw/official-docs/problem-detail-rfc-7807.md#RFC7807-C5` ("`detail` ... ought to focus on helping the client correct the problem, rather than giving debugging information"), `raw/company-tech-blogs/github-api-error-format.md#GH-ERR-C4` (validation error code 어휘 — `custom` 은 message-driven 의 escape hatch) | `official-standard + official-vendor-doc` | RFC7807-C5 는 "ought to" 약한 어조; SQL constraint name 차단은 raw 인용보다 보안 일반 원칙 — 별도 raw (예: OWASP error handling) 보강 권장 |
| D4 | 이 branch 의 TODO 도 Work Item Contract 준수; Decisionized Work Items 표가 canonical 승급 기준 | UNSUPPORTED_DECISION (project-internal process gate; 외부 표준 근거 없음) | N/A | process gate 가 문서에만 있으면 silent skip — `/lint` 또는 PR template 으로 enforce 필요 |
| D5 | error envelope shape — custom `{success, data, error.{code,category,message,retryable,details}, meta}` 채택, RFC 7807 ProblemDetail 명시적 거부 | `raw/official-docs/problem-detail-rfc-7807.md#RFC7807-C1` (canonical model 은 JSON `application/problem+json`), `#RFC7807-C2` (`type` URI 가 primary identifier — custom `code` 와 충돌), `#RFC7807-C3` (extension 가능하나 unknown 은 ignore), `raw/official-docs/spring-problem-detail.md#SPRING-PD-C1` (Spring `ProblemDetail` 은 RFC 9457 representation), `#SPRING-PD-C2` (모든 Spring MVC 예외가 `ErrorResponse` 구현 — ca-tmpl envelope 와 직접 충돌), `raw/company-tech-blogs/stripe-error-format.md#STRIPE-ERR-C5` (Stripe 4-종 type enum 사례), `raw/company-tech-blogs/toss-payments-error-format.md#TOSS-ERR-C1` (Toss `{code, message}` 평면 shape) | `official-standard + official-vendor-doc + company-case-study` | RFC 7807 미채택의 trade-off (표준 lock-in 회피 vs client 라이브러리 호환성) 는 인용된 source 들이 직접 권고하지 않음 — ca-tmpl 의 운영 해석. Stripe / Toss 는 company-case-study (best practice 격상 금지) |
| D6 | retryable 1급 필드 + success flag — 어떤 표준에도 1:1 매칭 없음 | `raw/official-docs/google-api-error-format.md#GOOG-ERR-C3` (details 에 typed payload — RetryInfo 등 포함 가능), `#GOOG-ERR-C5` (표준 detail payloads — BadRequest, ErrorInfo, LocalizedMessage 등), `raw/official-docs/graphql-errors-spec.md#GQL-ERR-C3` (partial response — `data` + `errors` 공존), `#GQL-ERR-C4` (extensions free-form map) | `official-vendor-doc + official-standard` | Google rpc.Status 만 retryable 을 detail 로 가짐 — top-level 1급 필드는 어떤 표준에도 없음 (ca-tmpl 고유 결정). GraphQL partial success 도 envelope success flag 와 다른 모델 |
| D7 | validation error mapping — syntax → VALIDATION, policy → AUTHZ/CONFLICT, invariant → CONFLICT/VALIDATION, persistence → DATA_INTEGRITY(null/FK/check)/CONFLICT(unique) | `raw/official-docs/json-api-errors-spec.md#JSONAPI-ERR-C3` (`source.pointer` JSON Pointer 로 field-level 오류 위치), `#JSONAPI-ERR-C5` (`title` 은 호출별 불변), `raw/company-tech-blogs/github-api-error-format.md#GH-ERR-C3` (validation 실패 = 422), `#GH-ERR-C4` (validation code 어휘 6개); category 명칭은 `ca-tmpl/docs/registries/error-codes.yaml` + `shared/error/Category.java` SSOT 확인 (2026-06-02) | `official-standard + official-vendor-doc + code-verified(category)` | category 분류 체계 자체의 외부 표준은 없음 — ca-tmpl 운영 결정. 실제 enum 은 10종 (VALIDATION/AUTH/AUTHZ/NOT_FOUND/CONFLICT/RATE_LIMIT/TRANSIENT_DEPENDENCY/PERMANENT_DEPENDENCY/DATA_INTEGRITY/INTERNAL); `PERSISTENCE` 는 없음 |
| D8 | duplicate validation 허용 — canonical owner 명시 시 UX/perf 사전 검증 가능 | UNSUPPORTED_DECISION (project-internal architectural decision; 외부 표준 근거 없음) | N/A | canonical owner 정합성은 PR 단위에서 review — silent duplication 위험 |
| D9 | validation details — safe field errors only; raw object/body/SQL detail 금지 | `raw/official-docs/problem-detail-rfc-7807.md#RFC7807-C5` ("`detail` ought to focus on helping the client correct the problem"), `raw/company-tech-blogs/github-api-error-format.md#GH-ERR-C5` (`custom` code 의 message-driven escape hatch — i18n 위험 명시) | `official-standard + official-vendor-doc` | "safe field error" 정의는 ca-tmpl 운영 해석; SQL/constraint name leak 차단은 일반 보안 원칙 — 별도 raw (OWASP) 보강 권장 |
## 구현 가이드
> *결정 (D1~D9)* 이 "*무엇* 을 검증할 것인가" 라면, 본 §는 "*어느 layer 에서 어떤 메커니즘으로*" 검증·변환·차단되는가의 사전 명세. 본 branch 의 핵심은 **검증 책임의 layer 배치** 다.
>
> error envelope 의 *shape* (D5/D6) 과 error code → HTTP → category *매핑 구현* (D7) 은 본 §에서 재명세하지 않는다 — sibling [[raw/branch-notes/feature-boundary-validation-mapping-contract]] §구현 가이드 + canonical [[raw/project-notes/ca-skeleton-operational-contract]] §6 이 소유 (R3 정제, §Audit & Findings 참조).
### 1. 4-layer validation 책임 배치 + 정적 강제
> **Trace**:
> - syntax/shape = controller boundary 전용 → **D1** (UNSUPPORTED_DECISION — 외부 표준이 boundary 별 책임 분할을 normative 강제하지 않음; sibling `feature-boundary-validation-mapping-contract` 와 동일 4-layer 결정 분산이므로 cross-review 필수). 검증은 §Claims To Verify row 1.
> - business invariant = domain model/value object 전용 → **D2** (UNSUPPORTED_DECISION — DDD aggregate wisdom, 인용 source 8개 중 normative 진술 없음). 검증은 §Claims To Verify row 2.
> - **use case policy layer 는 Decision Evidence Map 에 대응 D-ID 가 없음** (gap — §Audit & Findings F2). 아래 표 row 는 Decisionized Work Items 의 "use case policy" row 에서만 도출되며 외부 근거 미연결.
> - **UNSUPPORTED_IMPL_DECISION**: ①ArchUnit rule 이름 (`valid_only_in_controller`, `domain_invariant_on_all_mutations` 등) 임의 명명. ②"모든 mutation 경로" 의 조작적 정의 (생성자 / setter / 도메인 메서드 중 어디까지를 mutation 으로 보는지) — raw 권고 없음, 사용자 임의. ③layer 별 package glob (`..adapter.web..` / `..application..` / `..domain..`) — canonical [[raw/project-notes/ca-skeleton-operational-contract]] §20 Skeleton Blueprint package convention 에서 도출(SUPPORTED via canonical SSOT), glob 변환만 임의.
| layer | 검증 책임 | 배치 위치 | 정적 강제 (계획) | Trace |
| --- | --- | --- | --- | --- |
| syntax / shape | 입력 모양 (required / type / format / size) | `@Valid` + Bean Validation @ controller DTO (`..adapter.web..dto..`) | `@Valid` 가 controller package 밖에 등장하면 build 실패 (ArchUnit) | D1 |
| use case policy | application 권한·상태전이 정책 | application service (`..application..`) | 정적 강제 없음 — review-only (근거 없음, 아래 trade-off) | Decisionized WI "use case policy" (no D-ID, F2) |
| domain invariant | 비즈니스 불변식 | domain model / value object (`..domain..`) | invariant method 가 모든 mutation 경로에서 호출되는지 ArchUnit + bypass test | D2 |
| persistence integrity | unique / FK / 무결성 | infrastructure adapter → operational error 변환 (§2) | §2 참조 | D3 |
> - **UNSUPPORTED_IMPL_DECISION (policy layer 정적 강제 부재)**: use case policy 를 ArchUnit 으로 강제하지 않고 review-only 로 두는 것은 사용자 trade-off — application 정책은 도메인/요청 문맥 의존이 커서 정적 규칙의 false positive 가 많다는 판단. 근거 raw 없음.
### 2. Persistence integrity → operational error 변환 지점
> **Trace**: persistence integrity error 는 operational error 로 변환하되 client-safe message 만 응답 → **D3** + `raw/official-docs/problem-detail-rfc-7807.md#RFC7807-C5` ("`detail` ought to focus on helping the client correct the problem, rather than giving debugging information"), `raw/company-tech-blogs/github-api-error-format.md#GH-ERR-C4`. 검증은 §Claims To Verify row 3.
>
> - **메커니즘 (ground truth 2026-06-02, ca-tmpl 코드 확인)**: `src/adapter-web/.../error/GlobalExceptionHandler.java` (`@RestControllerAdvice`) + `ErrorResponseFactory` 는 `actually-implemented` 지만, 현재 `@ExceptionHandler` 목록(MappingException / IllegalArgumentException / ConstraintViolation / MethodArgumentTypeMismatch / InvalidBearerToken / Authentication / AccessDenied / PreconditionFailed / PageValidation / Cursor / Exception)에 **`DataIntegrityViolationException` 핸들러가 없음** — persistence integrity 변환은 `planned`. owner 는 [[raw/branch-notes/feature-persistence-failure-baseline]] (documented-only). 본 branch 는 그 핸들러를 *consume* 하며, integrity handler 추가는 owner branch 책임.
> - **카테고리 매핑 (registry SSOT, `ca-tmpl/docs/registries/error-codes.yaml`)**: unique 위반 → `CONFLICT` (`DB_UNIQUE_VIOLATION`, 409); null/FK/check 위반 → `DATA_INTEGRITY` (`DB_NULL_VIOLATION`/`DB_FK_VIOLATION`/`DB_CHECK_VIOLATION`). **`PERSISTENCE` enum 은 존재하지 않음** (`src/shared-contract/.../error/Category.java` 10-enum 확인).
> - **UNSUPPORTED_IMPL_DECISION**: SQL/constraint name 차단은 RFC7807-C5("ought to" 약한 어조)보다 *일반 보안 원칙* — 별도 raw (OWASP error handling) 보강 권장(D3 Open Risk).
- `DataIntegrityViolationException` / `OptimisticLockingFailureException` 등 persistence 예외는 infrastructure→presentation 으로 *raw 전파 금지*. exception handler 가 `DATA_INTEGRITY` (null/FK/check) 또는 `CONFLICT` (unique) category 의 operational error envelope 로 변환. **현재 미구현** — owner: `feature-persistence-failure-baseline`.
- 응답 `error.message` 는 client-safe 고정 문구만 (registry `client_safe_message`, 예: `DB_UNIQUE_VIOLATION` = "Resource already exists"). SQL 문장·constraint 이름·table/column 명을 `message`/`details` 어디에도 노출 금지.
- 구체적 envelope shape 은 본 branch 범위 밖 → canonical §6 (OUT_OF_BRANCH_SCOPE, §Audit F1).
### 3. Validation detail leakage 차단
> **Trace**: validation details — safe field errors only, raw object/body/SQL detail 금지 → **D9** + `#RFC7807-C5`, `raw/company-tech-blogs/github-api-error-format.md#GH-ERR-C5` (`custom` code 의 message-driven escape hatch — i18n/leak 위험). 검증은 §Claims To Verify row 7.
>
> - **UNSUPPORTED_IMPL_DECISION**: "safe field error" 의 *정의* (어떤 필드 메타까지 허용 — field 경로? rejected value 포함? message?) 는 ca-tmpl 운영 해석, raw 가 권고하지 않음.
- `details` 에 허용: field 경로 + validation message (i18n key). **금지**: 직렬화된 raw request object/body, SQLException message, stacktrace, constraint name.
- 의도적 `SQLException` 발생 → response body grep 으로 leak 회귀를 contract test 로 pin (§Claims row 7).
### 4. Duplicate validation canonical owner 표기
> **Trace**: duplicate validation 허용 — canonical owner 명시 시 UX/perf 사전 검증 가능 → **D8** (UNSUPPORTED_DECISION — project-internal architectural decision, 외부 근거 없음). 검증은 §Claims To Verify row 8.
>
> - **UNSUPPORTED_IMPL_DECISION**: owner 표기 메커니즘 (코드 주석 vs annotation vs 문서 표) 전부 사용자 임의 — D8 자체가 무근거이므로 detail 도 무근거.
- 같은 규칙을 두 layer 에서 검증하는 것은 허용하되, **canonical owner 를 명시**. owner 없는 duplicate rule 추가 시 silent contradiction → 금지.
- **기본값 (착수 가능 수준)**: 코드 주석 `// canonical-owner: <layer>` (예: `// canonical-owner: domain-invariant`) — 단순, 도구 불필요. duplicate 검증 지점마다 owner layer 한 줄 명시.
- (대안) annotation 강제(ArchUnit): [[raw/branch-notes/feature-boundary-validation-mapping-contract]] §Mapper Tool Contract 의 annotation 패턴 참조 후 별도 결정 — D8 무근거이므로 도입 여부는 review 판단.
## 엣지·실패·의존
> R4(깊이 게이트) 캡처용. 정상 경로 외 *구현 중 부딪힐* 실패/엣지/다른 계약 의존.
- **실패·엣지 경로**:
- malformed JSON (`HttpMessageNotReadableException`) vs Bean Validation 실패 (`MethodArgumentNotValidException`) — 둘 다 syntax layer 지만 *다른 exception*. 둘 다 `VALIDATION` category 로 수렴해야 함(sibling D10 과 정합).
- **동시성 하 unique constraint race**: application 사전 check(D8 duplicate)가 통과해도 DB 레벨에서 integrity violation 발생 가능 → persistence layer(D3)가 최종 방어선. 사전 check 는 UX 목적일 뿐 invariant 보장 아님.
- nested DTO `@Valid` cascade 깊이 — sibling 의 cascade depth ≤ 3 정적 강제(B4)에 의존.
- **다른 계약 의존**:
- [[raw/branch-notes/feature-boundary-validation-mapping-contract]] 의 `D10` (exception → error code → category 매핑) 을 consume — 본 branch 의 4-layer 가 *어느 category 로* 떨어지는지는 sibling 이 결정. sibling 매핑이 바뀌면 본 branch 의 §판정 기준 Required mapping 표가 영향.
- [[raw/project-notes/ca-skeleton-operational-contract]] §6 (Operational Error Category 통합 정의) + §20 (package convention) 을 consume — envelope shape·package glob 의 SSOT.
- **구현 순서 의존 (2026-06-02 ground truth)**: 본 branch 의 Claims row 3(persistence integrity 매핑)은 [[raw/branch-notes/feature-persistence-failure-baseline]] 가 `GlobalExceptionHandler``DataIntegrityViolationException` 핸들러를 구현한 *후에야* `planned``verified` 전환 가능. 현재 그 핸들러는 부재(코드 확인). policy AUTHZ 코드는 [[raw/branch-notes/feature-security-operational-baseline]] 소유.
## Audit & Findings
> R3 정제 history + 발견된 gap 보존 (§구현 가이드 본문에서 제외한 항목의 이관 근거).
| ID | 유형 | 내용 | 조치 |
| --- | --- | --- | --- |
| F1 | OUT_OF_BRANCH_SCOPE | D5 (envelope custom shape), D6 (retryable/success flag) 의 *구현 명세*`EnvelopeBodyAdvice`/`Envelope`/`BulkEnvelope` 클래스·factory API — 는 sibling [[raw/branch-notes/feature-boundary-validation-mapping-contract]] §구현 가이드 §4 + canonical §6 이 소유. 본 §구현 가이드에서 재명세 제외. D5/D6 결정 *기록* 은 Decision Evidence Map 에 유지(error-format Topic 4 공유 조사 산물). | sibling/canonical 참조로 대체 |
| F2 | DECISION_GAP | Decisionized Work Items 의 "use case policy" row + §1 표의 policy layer 가 Decision Evidence Map 에 대응 D-ID 가 없음. syntax(D1)/invariant(D2)/persistence(D3)/duplicate(D8)/details(D9)는 D-ID 보유하나 policy 만 누락. | **착수 기본값 (registry `owner_branch` 확인 2026-06-02)**: 인가 정책 violation → `AUTHZ` (실재 코드 `AUTHZ_INSUFFICIENT_PERMISSION` + `AUTHZ_TENANT_MISMATCH`, 403, **둘 다 owner `feature-security-operational-baseline`**); 상태 전이 충돌 → `CONFLICT`. (주의: `feature-tenant-context-policy` 는 AUTHZ 코드 소유자 아님 — `TENANT_NOT_SUPPORTED`(VALIDATION/400) 별도 소유.) 본 branch 는 이 코드들을 *consume*. **매핑 자체는 여전히 UNSUPPORTED** (본 노트 D-ID 없음) — 코드 착수 후 policy layer 책임을 D10(본 노트)로 승격하거나 owner branch 와 cross-link 하여 확정 필요. 추측을 FACT 로 기재 금지. |
| F3 | OUT_OF_BRANCH_SCOPE | D7 의 *코드→category 매핑 구현* 은 sibling D10 영역. 본 branch 는 *어느 layer 가 어느 category 후보인지* 의 책임 view 만 제공. persistence 코드(`DB_UNIQUE_VIOLATION`→CONFLICT, `DB_NULL/FK/CHECK_VIOLATION`→DATA_INTEGRITY)는 owner [[raw/branch-notes/feature-persistence-failure-baseline]] 소유. | sibling/owner 참조 |
| F4 | DEFERRED_RESEARCH | D1 (4-layer 분리), D2 (DDD invariant 책임) 의 외부 근거 보강 — D2 Open Risk 가 Vernon/Fowler(anemic vs rich domain) raw 인용을 명시. `wiki-decision-researcher` 자동조사 후보지만 web-fetch(outward) 라 사용자 opt-in 대기. | `/branch-spec ... --research D1,D2` 또는 수동 |
| F5 | CATEGORY_DRIFT → **RESOLVED 2026-06-02** | 본 노트가 쓰던 `PERSISTENCE` category 는 실재하지 않음 — `src/shared-contract/.../error/Category.java` 의 10-enum(VALIDATION/AUTH/AUTHZ/NOT_FOUND/CONFLICT/RATE_LIMIT/TRANSIENT_DEPENDENCY/PERMANENT_DEPENDENCY/DATA_INTEGRITY/INTERNAL)에 없음. registry `error-codes.yaml` 의 실제 매핑: `DB_UNIQUE_VIOLATION`→CONFLICT(409), `DB_NULL/FK/CHECK_VIOLATION`→DATA_INTEGRITY. (`error-codes.yaml:580` 주석에도 동일 stale 매핑이 전파돼 있었음 — ca-tmpl 레포 측 별도 정리 대상.) | **반영 완료**: D3/D7/§판정 기준/Decisionized WI/§구현 가이드 §2/§엣지/Claims 의 `PERSISTENCE``DATA_INTEGRITY(null/FK/check)/CONFLICT(unique)` 로 정합 (코드+registry 근거). |
## 검증해야 할 주장
> 공식 문서/사례는 근거지만 내 프로젝트에서의 동작을 자동 보장하지 않는다. 구현 전/중/후 실제로 검증해야 하는 주장.
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| request DTO validation 이 controller boundary 에서만 트리거되고 domain layer 로 새지 않는지 | `@Valid` annotation 위치 / interceptor 체인 misconfiguration 가능성 | ArchUnit rule (`@Valid` annotation 은 controller package 만) + integration test | `planned` |
| business invariant 가 domain model / value object 안에서 강제되며 application service bypass 불가한지 | service-layer invariant check 로 domain bypass 가능성 | ArchUnit rule (domain model 의 invariant method 가 모든 mutation 경로에서 호출) + 의도적 bypass test | `planned` |
| persistence integrity exception (e.g., `DataIntegrityViolationException`) 이 envelope 의 `DATA_INTEGRITY`(null/FK/check) / `CONFLICT`(unique) category 로 매핑되며 SQL/constraint name leak 안 되는지 | 현재 `GlobalExceptionHandler``DataIntegrityViolationException` 핸들러 자체가 없음(2026-06-02 확인) — owner `feature-persistence-failure-baseline` 미구현 | owner branch 구현 후 exception handler contract test + DLP scan (constraint name regex grep on response) | `planned` (owner: feature-persistence-failure-baseline) |
| 4-layer mapping (syntax → VALIDATION, policy → AUTHZ/CONFLICT, invariant → CONFLICT/VALIDATION, persistence → DATA_INTEGRITY/CONFLICT) 가 모든 exception 에 일관 적용되는지 | category 분류의 silent miscategorization 가능성 | exception → category 매핑 contract test (각 layer 의 대표 exception 별 category 검증) | `planned` |
| envelope 의 `retryable` 플래그가 category 와 정합한지 (registry 확인: VALIDATION/CONFLICT/DATA_INTEGRITY 모두 `retryable=false`) | retryable 은 per-code (registry `error-codes.yaml`), category 에서 계산 금지 (`Category.java` javadoc) | category × retryable matrix contract test + registry 대조 | `planned` |
| RFC 7807 ProblemDetail 미채택이 Spring 6 의 autoconfigure (`spring.mvc.problemdetails.enabled`, `SPRING-PD-C4`) 와 충돌하지 않는지 | Spring Boot default 가 true 인지 모름 → 자동 활성화 시 envelope override 필요 | sibling [[raw/branch-notes/feature-boundary-validation-mapping-contract]] 가 동일 위험을 `actually-implemented` 로 해소 (2026-05-29: `GlobalExceptionHandler``ProblemDetail` import 완전 제거, 우리 핸들러 우선이라 자동 활성화와 충돌 없음, `BoundaryDemoControllerWireTest` 11 케이스 wire-pin) → **본 branch 재검증 불필요** | `verified` (sibling) |
| `details` 필드에 raw object / body / SQL detail 이 절대 leak 안 되는지 | exception handler 의 detail 직렬화 path 에서 누락 가능 | leakage contract test (의도적 SQLException 발생 → response body grep) + production log scrub | `planned` |
| duplicate validation 의 canonical owner 가 코드 주석 / 문서에 명시되는지 | duplication 자체는 허용이지만 owner 누락 시 silent contradiction 가능 | code review checklist + ArchUnit rule (duplicate validator 는 owner annotation 필수) | `needs-confirmation` |
## 관심사 커버리지 (coverage-auditor 자동 생성)
> `/coverage` 가 생성하는 **생성물** — 손유지 금지. 기준: `rules/coverage-gate.md`. governing_docs: `clean-architecture-package-layout` + `api-error-envelope-design`.
> 마지막 감사: 2026-06-02 → **Covered** (Blocking 0 / Should-fix 0 / Advisory 4).
| 관심사 | 상태 | owner | 심각도 | 근거 |
|--------|------|-------|--------|------|
| 4-layer validation 책임 배치 (syntax/policy/invariant/persistence) | covered-here | — | — | D1·D2 |
| domain purity — infrastructure exception raw 전파 금지 | covered-here | — | — | D3 |
| @Valid 정적 강제 (controller 패키지 밖 금지) | covered-here | — | — | D1 (Claims row 1, planned) |
| business invariant violation → error.category 분류 | covered-here | — | — | D7 |
| persistence integrity → DATA_INTEGRITY(null/FK/check) / CONFLICT(unique) 매핑 | covered-here | — | — | D3·D7 (Audit F5 RESOLVED) |
| exception leak 금지 (SQL/constraint name/stacktrace) | covered-here | — | — | D9 |
| retryable 필드 정합 (VALIDATION/CONFLICT/DATA_INTEGRITY = false) | covered-here | — | — | Claims row 5 (registry SSOT) |
| web DTO containment — domain 직렬화 금지 | delegated | [[raw/branch-notes/feature-boundary-validation-mapping-contract]] D8 | — | owner `actually-implemented` (`controllers_do_not_return_domain_or_entity_types`) |
| validation 실패 → error.details[] 항목별 오류 매핑 | delegated | [[raw/branch-notes/feature-boundary-validation-mapping-contract]] D10 | — | owner `actually-implemented` (`VALIDATION_FAILED` details shape) |
| i18n 검증 메시지 정책 | governing 문서 비열거 | — | Advisory | 두 governing 문서 모두 미열거 — 프로젝트 레벨 owner 여부는 `/coverage --project` 영역 |
| 입력 정규화/sanitization before validation | governing 문서 비열거 | — | Advisory | 미열거. 실코드 trim/sanitize 는 header/pagination 맥락 |
| fail-fast vs collect-all 오류 수집 정책 | governing 문서 비열거 | — | Advisory | 미열거. 형제 B4 `@GroupSequence` 가 사실상 결정 |
| cross-field/conditional validation | governing 문서 비열거 | — | Advisory | 미열거. 형제 B4 class-level constraint `actually-implemented` |
## 완료 후 wiki 추출 대상
- `wiki/projects/ca-skeleton-operational-contract.md`의 business rule validation canonical section.
## 마주친 문제
- 아직 없음(문서 단계).
## 묶음
<!-- 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/problem-detail-rfc-7807]]
- [[raw/official-docs/spring-mvc-rest-exception-handling]]
- [[raw/official-docs/spring-problem-detail]]
- [[raw/official-docs/validation-jakarta-bean-validation-3.0-spec]]
<!-- GENERATED: sources:end -->
> 본 feature branch 는 leaf — 자식 자료 없음. Phase C2 실 코드 작성 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.
### 오류 기록 (본 feature 작업 중 발생)
- (없음 — 현재 documented-only 단계)
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- (없음 — Phase C2 실 구현 단계에 누적)
## 관련 일일 노트
> 이 브랜치를 작업한 날짜들. 양방향 nav 유지.
- (아직 없음 — documented-only 단계. 실 구현 착수 시 `[[raw/daily-notes/YYYY-MM-DD]]` 누적)
## 완료 후 정리
> 머지/종료 시점에 채움.
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
- `actually-implemented` 항목:
- `locally-verified` 항목:
- `prod-verified` 항목:
- **추출하지 않을 항목** (planned / documented-only / abandoned):