374 lines
36 KiB
Markdown
374 lines
36 KiB
Markdown
---
|
||
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):
|
||
|