478 lines
71 KiB
Markdown
478 lines
71 KiB
Markdown
---
|
|
title: branch / feature-boundary-validation-mapping-contract
|
|
source_type: branch-note
|
|
status: verified
|
|
branch: feature-boundary-validation-mapping-contract
|
|
parent_branch:
|
|
related_projects: [ca-skeleton]
|
|
tags: [branch, ca-skeleton, validation, mapper, boundary]
|
|
created: 2026-05-21
|
|
last_reviewed: 2026-06-04
|
|
target_merge:
|
|
status_label: in-progress
|
|
last_implementation_pass: 2026-05-29 (4th pass — Forbidden 정적 강제 + B5 sample 보강 + 문서 구현 가이드)
|
|
ingest_note: "2026-06-04 /ingest — ca-tmpl @fccb033 ground-truth 대조 후 verified. wiki/projects/ca-tmpl/boundary-validation-mapping.md + wiki/concepts/boundary-validation-and-dto-mapping.md 추출. 대조 결과: controller-return-type / valid_cascade_depth ArchUnit rule 은 노트의 planned 표기와 달리 fccb033 에 실제 구현됨(actually-implemented 로 격상). MappingException 위치는 노트 errors 로그의 application.exception 이 아니라 fccb033 에서 shared.error. sample 은 fccb033 에 이미 sample-portfolio(WorkLog), wire 테스트는 WorkLogControllerWireTest. ./gradlew test verifyCleanArchitectureDependencies → 126 tests / 0 failures."
|
|
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-002
|
|
kind: project-work-item
|
|
project: ca-skeleton-operational-contract
|
|
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-002
|
|
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MAPPING-001@1]
|
|
refines: []
|
|
overrides: []
|
|
depends_on: []
|
|
contract_packet: 1
|
|
contract_packet_sha256: 4c15ac1bd65a18209652e97e9c30583cf8326a361f4979f7fc588e0ab66cb67a
|
|
---
|
|
|
|
# branch: feature-boundary-validation-mapping-contract
|
|
|
|
> Layer: `raw/branch-notes/` — request/application/domain/response/filter 경계의 validation과 mapper 계약을 정의합니다.
|
|
|
|
<!-- 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: 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/arch-acl-microsoft-pattern]]
|
|
- [[raw/official-docs/google-api-error-format]]
|
|
- [[raw/official-docs/graphql-errors-spec]]
|
|
- [[raw/official-docs/json-api-errors-spec]]
|
|
- [[raw/official-docs/patch-json-merge-rfc7396]]
|
|
- [[raw/official-docs/problem-detail-rfc-7807]]
|
|
- [[raw/official-docs/runtime-spring-boot-virtual-threads]]
|
|
- [[raw/official-docs/schema-jackson-polymorphic-deserialization]]
|
|
- [[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 -->
|
|
|
|
<!-- GENERATED: errors:start -->
|
|
- [[raw/errors/mapping-exception-location-archunit-catch-2026-05-29]]
|
|
<!-- GENERATED: errors:end -->
|
|
|
|
<!-- GENERATED: blog-topics:start -->
|
|
- [[raw/blog-topics/archunit-jackson-default-typing-cve-2019-14379-block-2026-05-29]]
|
|
- [[raw/blog-topics/boundary-validation-mapper-responsibility-map-2026-07-02]]
|
|
- [[raw/blog-topics/identifier-governance-rule-scoping-by-id-kind-2026-06-01]]
|
|
<!-- GENERATED: blog-topics:end -->
|
|
|
|
> 본 feature branch 는 leaf — 자식 자료 없음. Phase C2 실 코드 작성 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.
|
|
|
|
### 근거 자료
|
|
|
|
- [[raw/official-docs/patch-json-merge-rfc7396]] — PATCH null=deletion IETF normative 근거 (B2 블라인드)
|
|
- [[raw/official-docs/arch-acl-microsoft-pattern]] — Microsoft Azure Architecture Center ACL 패턴 공식 정의. outbound HTTP 응답 → domain 변환이 mapper 범위 안에 포함된다는 scope 명확화 근거 (블라인드 B7).
|
|
- [[raw/official-docs/spring-mvc-rest-exception-handling]] — Spring MVC `ErrorResponse` 계약, `ResponseEntityExceptionHandler` 처리 예외 목록, `HttpMessageNotReadableException` / `MethodArgumentNotValidException` normative 처리 근거 (B3 블라인드 해소)
|
|
- [[raw/official-docs/validation-jakarta-bean-validation-3.0-spec]] — Jakarta Bean Validation 3.0 normative spec. class-level constraint 목적, group sequence short-circuit, @Valid cascade, TYPE_USE container element 위치 정의 (B4 블라인드 해소)
|
|
- [[raw/official-docs/runtime-spring-boot-virtual-threads]] — Spring Boot 공식 레퍼런스: `spring.threads.virtual.enabled` semantics + virtual thread 활성화 시 executor/scheduler 전환 근거 (B6 블라인드)
|
|
- [[raw/official-docs/schema-jackson-polymorphic-deserialization]] — Jackson polymorphic deserialization 보안 지침. `enableDefaultTyping()` 금지 (`@Deprecated` since 2.10) + `PolymorphicTypeValidator` / `BasicPolymorphicTypeValidator` 공식 allowlist API + CVE-2019-14379 gadget chain RCE 근거 (B5 블라인드 해소)
|
|
|
|
### 오류 기록 (본 feature 작업 중 발생)
|
|
|
|
- [[raw/errors/mapping-exception-location-archunit-catch-2026-05-29]] — B7 outbound ACL 매퍼가 `MappingException` 을 던지자 `outbound_adapter_does_not_depend_on_web_or_persistence_adapters` ArchUnit 규칙이 cross-adapter 의존을 catch. 해소 = sentinel 을 `application.exception` 으로 이전. *fitness function 이 contract 변경 비용을 정확히 측정한 정상 동작* 의 기록.
|
|
- (Jackson `DeserializationFeature` enum 이 app-bootstrap 의 test classpath 에 없어 컴파일 실패한 1회는 `testImplementation 'spring-boot-starter-json'` 추가로 해소 — 1회성 환경 정렬이므로 `raw/errors/` 등재 생략.)
|
|
|
|
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
|
|
|
- (해당 enforcement 패스에서 면접 질문 단독 추출 없음. RFC 7807 거부 + custom envelope, CVE-2019-14379 + ArchUnit 정적 차단 같은 질문 후보는 sibling branch `feature-business-rule-validation-contract` 의 cluster 와 신규 [[raw/blog-topics/archunit-jackson-default-typing-cve-2019-14379-block-2026-05-29]] 에서 다룰 영역과 중복.)
|
|
|
|
### Blog topics (이 작업에서 파생)
|
|
|
|
- [[raw/blog-topics/archunit-jackson-default-typing-cve-2019-14379-block-2026-05-29]] — Jackson `enableDefaultTyping()` / `LaissezFaireSubTypeValidator` 의 RCE 게이트를 ArchUnit fitness function 으로 정적 차단한 1차 enforcement 사례.
|
|
|
|
<!-- GENERATED: branch-contract:start -->
|
|
<!-- section-id: branch-contract-packet -->
|
|
## 브랜치 계약 패킷
|
|
|
|
- **생성 시 프로젝트 개정**: `1`
|
|
- **패킷 스키마**: `contract_packet: 1`
|
|
- **완료 조건**: boundary·mapping 6필드 contract와 negative fixture가 통과한다
|
|
|
|
<!-- 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]] |
|
|
|
|
<!-- 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 -->
|
|
## 목표
|
|
|
|
CA skeleton에서 경계가 흐려지면 DTO, domain object, persistence model이 서로 새어 나갑니다. 이 branch는 각 경계가 무엇을 검증하고 어떤 mapper를 통과해야 하는지 고정합니다.
|
|
|
|
- 이슈:
|
|
- PR:
|
|
|
|
<!-- section-id: branch-scope -->
|
|
## 범위
|
|
|
|
### 포함 범위
|
|
|
|
- request DTO validation.
|
|
- request DTO -> application command/query mapper.
|
|
- application command/query invariant validation.
|
|
- domain object -> response DTO 직접 노출 금지.
|
|
- response mapper public field 정책.
|
|
- filter/interceptor request context propagation.
|
|
|
|
### 제외 범위
|
|
|
|
- 특정 도메인 validator 구현.
|
|
- DB/JPA exception mapping.
|
|
- outbound adapter retry 구현.
|
|
|
|
## TODO
|
|
|
|
> TODO drained 2026-05-22 — 결정은 아래 "결정 사항" / "Mapper Tool Contract" / "판정 기준" / "테스트 계약" 참조. request DTO validation/request→command mapper/application invariant/domain object 노출 금지/response mapper 정책/filter context/boundary 우회 탐지 모두 결정 라인 또는 표 row로 반영됨. 잔존 TODO 없음.
|
|
|
|
> 본 branch는 Mapper Tool Contract와 validation 4-layer 분류 자체가 결정 표 등가. 별도 Decisionized Work Items 표는 작성하지 않음.
|
|
|
|
## 진행 중 메모
|
|
|
|
- mapper는 단순 변환기가 아니라 허용/차단/정규화/마스킹 경계입니다.
|
|
|
|
## 결정 사항 (decisions)
|
|
|
|
- 2026-05-21: 모든 경계에 validation/mapping 책임을 둠.
|
|
- 2026-05-22: validation은 syntax, policy, invariant, persistence integrity로 책임을 분리.
|
|
- 2026-05-22: mapper는 변환뿐 아니라 normalization, masking, public field selection의 경계로 취급.
|
|
- 2026-05-22: mapper 도구 기본값은 수기 mapper + record canonical constructor. MapStruct는 optional이며 사용 시 generated code architecture exemption과 mapper contract test가 필요.
|
|
- 2026-05-28: (B1) Jackson deserialization 정책 — `spring.jackson.deserialization.fail-on-unknown-properties=true` 명시 (Jackson default 와 동일, 회귀 방지). `FAIL_ON_NULL_FOR_PRIMITIVES=true` 또는 request DTO 가 wrapper type (`Integer`, `Long`, `Boolean`) 만 사용. 클래스 단위 `@JsonIgnoreProperties(ignoreUnknown=true)` 는 ArchUnit rule 로 금지.
|
|
- 2026-05-28: (B2) PATCH 요청 mapper 는 RFC 7396 의 null=deletion semantics 를 채택하지 않음 (envelope success/error 대칭 정책과 충돌). PATCH endpoint 는 absent 필드 = 변경 없음 / null 필드 = 명시적 null 의미로 처리하며, `JsonNullable` (openapi-generator) 또는 `Optional<T>` wrapper 로 absent vs null 을 구분. RFC 7396 미채택 사실을 OpenAPI 문서에 명시.
|
|
- 2026-05-28: (B3) Mapping exception 분류 — `HttpMessageNotReadableException` / `MethodArgumentNotValidException` 은 Spring `ResponseEntityExceptionHandler` 가 normative 처리하므로 `VALIDATION` 카테고리. mapper-internal 예외 (`IllegalArgumentException`, record canonical constructor `IllegalStateException`, MapStruct generated NPE) 는 별도 `@ExceptionHandler` 에서 잡아 ca-tmpl operational contract 의 `MAPPING_FAILED` 신규 code 로 분류 (canonical SSOT §6 갱신 필요).
|
|
- 2026-05-28: (B4) Cross-field 와 class-level Bean Validation 의 책임 — class-level constraint 는 syntax 레이어 (request DTO 의 multi-property 형식 검증), domain invariant 는 application/domain layer 의 별도 검증. `@GroupSequence` 로 syntax → invariant 단계 short-circuit 패턴 채택. `@Valid` cascade depth 는 ArchUnit / runtime limit 으로 nested 3 단계 이내 제한.
|
|
- 2026-05-28: (B5) Polymorphic deserialization — `ObjectMapper.enableDefaultTyping()` / `activateDefaultTyping(LaissezFaireSubTypeValidator)` 금지 (ArchUnit). sealed `Command` interface + record subtypes 는 `@JsonTypeInfo(use = NAME)` + `@JsonSubTypes` 명시 또는 `BasicPolymorphicTypeValidator` allowlist 로만 deserialize.
|
|
- 2026-05-28: (B6) Virtual thread — `spring.threads.virtual.enabled=true` 활성화 시 Tomcat connector / `@Async` executor 가 `SimpleAsyncTaskExecutor` 로 전환되므로 filter/interceptor 의 `ThreadLocal` 기반 context propagation (`RequestContextHolder`, MDC) 안전성을 contract test 로 검증. MDC 는 SLF4J 2.0+ (Loom 호환) 또는 Micrometer Context Propagation 위임. `InheritableThreadLocal` 사용 금지.
|
|
- 2026-05-28: (B7) 본 branch 의 mapper 범위는 inbound `request→application` + `application→response` 뿐 아니라 outbound `external-response→domain` 도 포함 (ACL 패턴). outbound adapter 의 응답 → domain 변환에도 동일한 normalization / masking / public field selection 책임이 적용된다. ACL 의 inline (인-프로세스) 구현은 허용, 별도 서비스 추출은 out-of-scope.
|
|
- 2026-05-28: (B8) Bulk endpoint 의 partial success — envelope 의 top-level `success` flag 는 *전체 성공* 시에만 true. 부분 실패는 `success: false` + `error.code = BATCH_PARTIAL_FAILURE` + `error.details[]` 에 항목별 결과 배열. 단일 항목 endpoint 와 schema 가 다르므로 OpenAPI 에서 별도 response shape 으로 분기. Google rpc.Status typed details / JSON:API errors[] / GraphQL data+errors 패턴이 선례.
|
|
- (B9) Resource identifier ArchUnit rules cross-cite — [[raw/branch-notes/feature-resource-identifier-contract]] D17 의 5개 rule (`no_long_id_pk`, `no_uuid_random_in_controller`, `no_math_random_for_id`, `no_varchar_255_for_id_column`, `no_find_by_id_without_tenant`) 를 본 branch 의 ArchUnit suite 에 등록. 구현 skeleton 은 resource-identifier branch §구현 가이드 §6. ArchUnit version = archunit-junit5 1.3.0 per project §34 Stack Commitment.
|
|
|
|
## 근거 (필수, 최소 1개+)
|
|
|
|
> 본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.
|
|
|
|
| Source | 정당화하는 결정 |
|
|
|---|---|
|
|
| [[raw/official-docs/schema-jackson-unknown-field-handling]] | Jackson `DeserializationFeature` default 4종 — B1 블라인드: request boundary 직전 `FAIL_ON_UNKNOWN_PROPERTIES` / `FAIL_ON_NULL_FOR_PRIMITIVES` / `FAIL_ON_IGNORED_PROPERTIES` / `READ_UNKNOWN_ENUM_VALUES_AS_NULL` 정책 강제 근거 |
|
|
| [[raw/official-docs/schema-jackson-polymorphic-deserialization]] | Jackson polymorphic deserialization 보안 지침 — B5 블라인드: `enableDefaultTyping()` 금지 (`@Deprecated` 2.10) + `PolymorphicTypeValidator` allowlist + CVE-2019-14379 gadget chain RCE 근거 |
|
|
| [[raw/official-docs/patch-json-merge-rfc7396]] | PATCH null=deletion IETF normative semantics — B2 블라인드: null vs absent 구분 강제 근거 |
|
|
| [[raw/official-docs/spring-mvc-rest-exception-handling]] | Spring MVC `ErrorResponse` 계약, `ResponseEntityExceptionHandler` 처리 예외 목록 — B3 블라인드: `HttpMessageNotReadableException` / `MethodArgumentNotValidException` → `VALIDATION` 분류 근거 |
|
|
| [[raw/official-docs/validation-jakarta-bean-validation-3.0-spec]] | Jakarta Bean Validation 3.0 normative — B4 블라인드: class-level constraint, group sequence short-circuit, `@Valid` cascade, TYPE_USE container element 위치 정의 |
|
|
| [[raw/official-docs/runtime-spring-boot-virtual-threads]] | Spring Boot `spring.threads.virtual.enabled` + virtual thread executor/scheduler 전환 — B6 블라인드: filter/interceptor `ThreadLocal` context propagation 안전성 |
|
|
| [[raw/official-docs/arch-acl-microsoft-pattern]] | ACL 패턴 공식 정의 — B7 블라인드: outbound HTTP 응답 → domain 변환이 mapper 범위 안에 포함된다는 scope 명확화 |
|
|
| [[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급. B8 블라인드: typed details 다형성으로 bulk partial-result 표현 |
|
|
| [[raw/official-docs/json-api-errors-spec]] | B8 블라인드: 다중 error 객체 배열 — bulk partial success 표현 |
|
|
| [[raw/official-docs/graphql-errors-spec]] | partial success 1급. B8 블라인드: data + errors 공존 모델 |
|
|
| [[raw/company-tech-blogs/github-api-error-format]] | — |
|
|
|
|
## 판정 기준
|
|
|
|
| 구분 | 기준 |
|
|
| --- | --- |
|
|
| Decision | 모든 외부 입력/출력은 mapper와 validation 경계를 통과 (B7: outbound 응답 → domain ACL mapper 포함) |
|
|
| Allowed | 단순 query DTO도 mapper를 거쳐 command/query로 변환. MapStruct는 optional generated mapper로만 허용. sealed `Command` interface 의 polymorphic deserialization 은 `@JsonTypeInfo` + `@JsonSubTypes` 또는 `BasicPolymorphicTypeValidator` allowlist 로만 허용 (B5). PATCH endpoint 는 absent vs null 구분 mapper 만 허용 (B2) |
|
|
| Forbidden | request DTO -> domain 직접 생성, domain/persistence model -> response 직접 반환, mapper 없는 public field 노출, `ObjectMapper.enableDefaultTyping()` / `activateDefaultTyping(LaissezFaireSubTypeValidator)` 호출 (B5), 클래스 단위 `@JsonIgnoreProperties(ignoreUnknown=true)` (B1), `InheritableThreadLocal` 직접 사용 (B6), RFC 7396 `application/merge-patch+json` content type 사용 (B2 — 미채택), outbound 응답 raw → domain 직접 mapping (B7 — ACL bypass) |
|
|
| Required validation | request syntax (class-level constraint 포함), command/query invariant, use case policy, domain invariant, response public field. `@GroupSequence` 로 syntax → invariant short-circuit (B4). `@Valid` cascade depth ≤ 3 (B4) |
|
|
| Failure condition | 경계 우회로 private/internal field가 응답에 노출되거나 domain invariant가 bypass되면 실패. mapper-internal exception 이 `MAPPING_FAILED` 가 아닌 `INTERNAL` 로 분류되면 실패 (B3). bulk endpoint 의 부분 실패가 `success: true` 로 반환되면 실패 (B8). virtual thread 환경에서 `requestId`/`traceId`/MDC 가 application layer 까지 propagate 되지 않으면 실패 (B6) |
|
|
|
|
## 구현 가이드
|
|
|
|
> *문서가 모호해서 구현자가 임의로 정해야 했던 결정* 카탈로그.
|
|
|
|
### 1. Error code → HTTP status → retryable 표 (B3/B8 보강)
|
|
|
|
> **Trace**: 본 표의 row 는 모두 본 branch (boundary/validation/mapping) 결정 영역. 도메인 특화 code (예: `USER_NOT_FOUND`) 와 다른 branch 결정 영역 (security/conflict/infra-failure) 의 row 는 [[raw/project-notes/ca-skeleton-operational-contract]] §6 Operational Error Category 통합 정의 — 본 표는 *§6 의 부분 view*.
|
|
>
|
|
> - `VALIDATION_FAILED` → **D10 + `SPRING-MVC-EXC-C1/C4/C5`, `JBV-3.0-C5`**
|
|
> - `MAPPING_FAILED` → **D10** (canonical SSOT §6 등록 완료 2026-05-29)
|
|
> - `BATCH_PARTIAL_FAILURE` → **D14 + `GOOG-ERR-C3`, `GQL-ERR-C3`, `JSONAPI-ERR-C1`** (HTTP 200 은 partial-response 선례 차용; canonical SSOT §6 등록 완료 2026-05-29)
|
|
|
|
| code | HTTP | retryable | 의미 | 사용 |
|
|
| --- | --- | --- | --- | --- |
|
|
| `VALIDATION_FAILED` | 400 | false | Bean Validation 실패, JSON 파싱 실패, unknown field, 알 수 없는 enum value, polymorphic discriminator 불일치 — D10 의 "VALIDATION 카테고리" 일체 | `HttpMessageNotReadableException`, `MethodArgumentNotValidException`, `ConstraintViolationException` 모두 라우팅 |
|
|
| `MAPPING_FAILED` | 400 | false | Mapper-internal 실패 (record canonical constructor `IllegalArgumentException` wrap, MapStruct NPE, ACL normalization 실패). 반드시 `MappingException` 으로 명시적 wrap. | `handleMapping` |
|
|
| `BATCH_PARTIAL_FAILURE` | 200 | false | Bulk endpoint 의 부분/전체 실패. HTTP 200 + envelope.success=false (단일 항목 endpoint 와 *동일* 응답 표면, *분기는 envelope.success* 로). | `BulkEnvelope.partial(...)` |
|
|
|
|
> **`MALFORMED_REQUEST` 는 제거되었다.** 초기 구현은 unknown field 를 `MALFORMED_REQUEST`(400) 로 매핑했으나 D10 의 "HttpMessageNotReadableException → VALIDATION category" 와 본 branch §테스트 계약 "(B1) 400 + VALIDATION_FAILED" 와 충돌. `VALIDATION_FAILED` 로 통합하고 *구체적 실패 모드*(UnrecognizedPropertyException / InvalidTypeIdException / JsonParseException 등) 는 `error.details.cause` 로 surface 한다.
|
|
|
|
### 2. `error.details` shape (코드별)
|
|
|
|
> **Trace**: 본 표는 §1 의 in-scope row 와 1:1 대응. 도메인/HTTP-표준 row 의 shape 는 [[raw/project-notes/ca-skeleton-operational-contract]] §6 통합 정의.
|
|
>
|
|
> - `VALIDATION_FAILED (MethodArgumentNotValid)` → **Spring `FieldError` API 표준** (`SPRING-MVC-EXC-C5` 의 message arg `{1}=field errors` 차용)
|
|
> - `VALIDATION_FAILED (ConstraintViolation)` → **Jakarta `ConstraintViolation` API 표준** (`JBV-3.0-C2`)
|
|
> - `VALIDATION_FAILED (HttpMessageNotReadable)` → **D11 + `SJUF-C1~C4`** (Jackson exception 종류)
|
|
> - `BATCH_PARTIAL_FAILURE` → **D14 + `GOOG-ERR-C3` typed details / `JSONAPI-ERR-C1` errors array 패턴**
|
|
> - **UNSUPPORTED_IMPL_DECISION**: field 영문 키 이름 (`cause`, `index`, `status`, `id` 등) — 근거 raw 가 *구조* 는 권고하나 *키 이름* 은 권고하지 않음. OpenAPI 정의 시 명시 필요.
|
|
|
|
| code | `details` shape |
|
|
| --- | --- |
|
|
| `VALIDATION_FAILED` (from `MethodArgumentNotValidException`) | `List<{field, rejectedValue, message}>` (Spring `FieldError`) |
|
|
| `VALIDATION_FAILED` (from `ConstraintViolationException`) | `List<{field, message}>` |
|
|
| `VALIDATION_FAILED` (from `HttpMessageNotReadableException`) | `{cause: <Jackson exception simple-name>}` |
|
|
| `BATCH_PARTIAL_FAILURE` | `List<BulkItemResult{index, status, id, code, message}>` |
|
|
| 그 외 | `null` |
|
|
|
|
> OpenAPI 분기는 `oneOf` 로 표현. OpenAPI 스펙 자체가 부재해서 구현은 보류 — 별도 PR.
|
|
|
|
### 3. `MappingException` 라우팅 규약
|
|
|
|
> **Trace**: mapper-internal 라우팅 흐름은 **D10 직접 권고** (mapper-internal 예외 분류).
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: ①`MappingException` 이라는 *wrap 클래스 이름* (D10 은 wrap 강제만 권고, 클래스명은 임의). ②"정적 강제는 두지 않음" trade-off (false positive 우려 + mapper 코드 양이 적어 review 로 충분이라는 *사용자 판단*) — 근거 raw 없음, *trade-off articulation 기록* 으로 보존.
|
|
|
|
- Mapper 내부 (web/outbound/persistence ACL 어디든) 가 던진 *논리적 mapping 실패* 는 반드시 `MappingException` 으로 **wrap 해서** 던진다. 그러면 `handleMapping` 이 `MAPPING_FAILED` 로 라우팅.
|
|
- 이 규약을 *컨벤션* 으로 두고 정적 강제는 두지 않는다. 정적 강제는 너무 광범위해서 false positive 가 많고, mapper 코드는 양이 적어 review 로 충분하다는 판단.
|
|
|
|
### 4. Envelope wrap 적용 범위
|
|
|
|
> **Trace (audit 2026-05-29)**:
|
|
> - **In-scope**: 모든 `@RestController` 응답 `Envelope<T>` 자동 wrap 자체 → **D6 직접 권고** (success flag + envelope 대칭). `BulkEnvelope` pass-through → **D14 직접 권고** (bulk partial success shape 분리).
|
|
> - **HTTP 표준 차용**: DELETE / 204 No Content body skip — HTTP 표준, 본 branch 결정 외 자연 결과.
|
|
> - **UNSUPPORTED_IMPL_DECISION**: ①`EnvelopeBodyAdvice` 의 *Spring `ResponseBodyAdvice` 메커니즘 선택* 자체 (D6 는 wrap 만 권고, 메커니즘은 임의). ②컨트롤러 직접 반환 pass-through 로직 (재wrap 방지). ③`Envelope`/`BulkEnvelope` 라는 클래스 명명. ④`Envelope.ok(...)`, `BulkEnvelope.partial(...)`, `BulkEnvelope.allOk(...)` 의 *static factory API 모양* — D6/D14 가 권고하지 않음, 사용자 임의 design.
|
|
> - **운영 영향 anchor**: probe / monitoring 이 `$.status` → `$.data.status` 로 갱신 필요 — *근거 기반 결정의 운영 영향* 으로 §11 운영 회복력 검토 후보.
|
|
|
|
- *모든* `@RestController` 응답 (sample-portfolio 의 도메인 컨트롤러 + production `HealthcheckController` 포함) 은 `EnvelopeBodyAdvice` 가 자동으로 `Envelope<T>` 로 wrap.
|
|
- 컨트롤러가 직접 `Envelope.ok(...)` 반환하면 advice 가 *재wrap 하지 않음* (pass-through). 명시적 envelope 구성이 필요한 경우 직접 반환 OK.
|
|
- `BulkEnvelope<T>` 도 advice 의 pass-through 대상 — bulk 엔드포인트는 직접 `BulkEnvelope.partial(...)` / `BulkEnvelope.allOk(...)` 반환.
|
|
- DELETE / 204 No Content 는 body 가 없으므로 wrap 대상이 아님 (advice 가 null body skip).
|
|
- 운영 영향: probe / monitoring 이 `$.status` 같은 평탄 path 를 직접 읽고 있었다면 `$.data.status` 로 갱신 필요.
|
|
|
|
### 5. Cascade depth ≤ 3 정적 강제 메커니즘 (B4-2)
|
|
|
|
> **Trace (audit 2026-05-29)**:
|
|
> - **In-scope**: `@Valid` cascade depth ≤ 3 결정 자체 → **본 branch B4 결정 라인 + D2 (4-layer validation)** 직접 권고 ("ArchUnit / runtime limit 으로 nested 3 단계 이내 제한"). `JBV-3.0-C4` (`@Valid` cascade) 가 *cascade 메커니즘* 을 normative 로 다룸 → depth limit 자체는 본 branch 의 trade-off 결정 (DoS 방어).
|
|
> - **UNSUPPORTED_IMPL_DECISION**: ①ArchUnit rule 이름 `valid_cascade_depth_at_most_three` (임의 명명). ②depth 계산 algorithm (직접 `@Valid` 필드 = depth 1, 재귀 depth +1) — 근거 raw 가 depth 의 *조작적 정의* 를 권고하지 않음, 사용자 임의 정의. ③외부 라이브러리 (`java.*`, `jakarta.*`) cascade 무시 — false positive 회피의 사용자 trade-off, 근거 없음. ④limit 값 `3` 자체 — 1, 5, 7 도 가능했으나 사용자 임의 선택 (DoS 위험과 표현력의 균형 판단).
|
|
|
|
- ArchUnit `valid_cascade_depth_at_most_three` 규칙이 `..adapter.web..dto..` 패키지 클래스의 `@Valid` 필드를 재귀 따라가며 도메인 내부 클래스 사이의 cascade 깊이를 계산.
|
|
- depth 1 = 직접 `@Valid` 필드. depth 2 = `@Valid` 필드의 `@Valid` 필드. 등등.
|
|
- 외부 라이브러리 (`java.*`, `jakarta.*`) 로의 cascade 는 무시 (자기 도메인 외부는 depth 측정 안 함).
|
|
- 위반 시 build 실패. 신규 nested DTO 작성 시 양 3 단계 안에서 펼치거나 별도 매퍼/validator 로 분리.
|
|
|
|
### 6. Polymorphic deserialize 정적 강제 좁힘 (B5)
|
|
|
|
> **Trace (audit 2026-05-29)**:
|
|
> - **In-scope (strong)**: `enableDefaultTyping()` 차단 → **D12 + `JACK-POLY-C3`** (`enableDefaultTyping()` 2.10 `@Deprecated`, 대체 `activateDefaultTyping(PolymorphicTypeValidator)`); `LaissezFaireSubTypeValidator` 차단 → **D12 + `JACK-POLY-C1` + `JACK-POLY-C5`** (gadget chain CVE-2019-14379 normative 위험); `activateDefaultTyping(BasicPolymorphicTypeValidator)` 허용 → **D12 + `JACK-POLY-C4`** (allowlist 표준 구현체).
|
|
> - **UNSUPPORTED_IMPL_DECISION**: ①ArchUnit rule 이름 `no_jackson_enable_default_typing_call`, `no_jackson_laissez_faire_subtype_validator` (임의 명명). ②sample-portfolio 의 `BasicPolymorphicTypeValidatorAllowlistTest` 의 *4-case 선택* (Cat, Dog, 비허용 subtype, 임의 JDK 클래스) — pin 패턴의 사용자 임의 design, raw 가 권고하지 않음.
|
|
> - **참고**: 본 sub-section 은 모든 in-scope 결정이 normative claim 으로 지원되는 *가장 깨끗한* sub-section. 다른 sub-section 의 audit 기준점으로 사용 가능.
|
|
|
|
- `enableDefaultTyping()` (no-arg, deprecated) 호출 → 차단 (ArchUnit `no_jackson_enable_default_typing_call`).
|
|
- `LaissezFaireSubTypeValidator` 클래스 참조 → 차단 (ArchUnit `no_jackson_laissez_faire_subtype_validator`).
|
|
- `activateDefaultTyping(BasicPolymorphicTypeValidator allowlist)` → **허용**. 차단 대상 아님. 안전한 allowlist 패턴이며 `sample-portfolio` 의 `BasicPolymorphicTypeValidatorAllowlistTest` 가 4 case 로 pin (allowlisted Cat/Dog 통과, 비허용 subtype 거부, 임의 JDK 클래스 거부).
|
|
- 두 가지 정적 강제 + 두 가지 sample (sealed `@JsonTypeInfo`/`@JsonSubTypes` 와 `BasicPolymorphicTypeValidator`) 모두 D12 에 기록된 normative 패턴.
|
|
|
|
### 7. Controller 반환 / Application 파라미터 정적 강제 (§Forbidden 직접 강제)
|
|
|
|
> **Trace (audit 2026-05-29)**:
|
|
> - **In-scope (decision)**: controller return type 차단 → **D1 + D8** (response mapper public field 만 노출, domain 직접 노출 금지). application method DTO 파라미터 차단 → **D7** (request DTO → command/query mapper 강제).
|
|
> - **Decision Evidence 강도 한계**: D1, D8 의 Supporting Claims 는 *부분 normative* — `RFC7807-C5` (debug 정보 분리 사상), `JSONAPI-ERR-C5` (호출별 불변 사상) 이 *원칙* 만 권고, ArchUnit 강제는 직접 도출 X. D7 도 `RFC7396-C2~C4` 가 PATCH semantics 만 다룸. **즉 정적 강제 *메커니즘 자체* 는 사용자 trade-off 결정** (review-only vs static enforcement).
|
|
> - **Cross-reference 보강**: 패키지 패턴 `..domain.entity..`, `..adapter.persistence.entity..`, `..adapter.web..dto..` 의 *정확한 glob* → [[raw/project-notes/ca-skeleton-operational-contract]] §20 Skeleton Blueprint Contract 의 package convention 에서 도출. trace: SUPPORTED via canonical SSOT.
|
|
> - **UNSUPPORTED_IMPL_DECISION**: ①ArchUnit rule 이름 `controllers_do_not_return_domain_or_entity_types`, `application_methods_do_not_accept_web_dtos`. ②package glob 의 *정확한 `..` wildcard 위치* (canonical SSOT 의 anchor 와 일치하지만 glob 변환은 사용자 결정).
|
|
|
|
- `controllers_do_not_return_domain_or_entity_types` — controller method 반환 타입이 `..domain.entity..` 또는 `..adapter.persistence.entity..` 또는 `..repository..` 에 거주하면 build 실패. `EnvelopeBodyAdvice` 의 자동 wrap 이 도메인 객체를 silent 직렬화하는 회귀를 *정적* 으로 차단.
|
|
- `application_methods_do_not_accept_web_dtos` — application package 의 public method 가 `..adapter.web..dto..` 파라미터를 받으면 build 실패. controller 가 DTO → Command/Query 변환을 우회하는 회귀 차단.
|
|
|
|
### 8. ProblemDetail + RFC 7396 정적 강제 (D5 + B2)
|
|
|
|
> **Trace (audit 2026-05-29)**:
|
|
> - **In-scope (strong)**: ProblemDetail import 차단 → **D5 직접 결정** + `SPRING-PD-C1` (Spring `ProblemDetail` = RFC 9457 representation), `SPRING-PD-C2` (모든 Spring MVC 예외가 `ErrorResponse` 구현 — envelope 와 충돌), `SPRING-MVC-EXC-C1` (corroborate). 정적 강제 *목표* 는 SUPPORTED.
|
|
> - **In-scope (partial)**: `application/merge-patch+json` content type 차단 → **D7 (B2 결정 라인)** + `RFC7396-C2` (null=deletion normative), `RFC7396-C3` (explicit null 부적합 경고). RFC 7396 의 *미채택 결정* 자체가 ca-tmpl envelope 정책 (D5, D6) 과 정합 — content type 차단으로 정적 강제.
|
|
> - **UNSUPPORTED_IMPL_DECISION**: ①ArchUnit rule 이름 `no_problem_detail_usage`, `no_merge_patch_json_media_type_string` (임의 명명). ②import-level 차단 vs class-reference 차단 vs annotation-value 차단의 *메커니즘 선택* — D5/D7 이 직접 권고하지 않음, false positive vs 회귀 차단의 사용자 trade-off.
|
|
|
|
- `no_problem_detail_usage` — `org.springframework.http.ProblemDetail` import 자체를 차단. D5 의 "RFC 7807 명시적 거부" 가 코드 단계에서 강제됨. 신규 작업자가 무심코 `ProblemDetail` 을 부활시키면 build 실패.
|
|
- `no_merge_patch_json_media_type_string` — `@RequestMapping(consumes="application/merge-patch+json")` 같은 RFC 7396 도입을 build 실패로 차단. B2 의 "RFC 7396 미채택" 정적 강제.
|
|
|
|
## Mapper Tool Contract
|
|
|
|
| item | default |
|
|
| --- | --- |
|
|
| mapper implementation | 수기 mapper |
|
|
| command/query normalization | record canonical constructor 또는 static factory |
|
|
| generated mapper | MapStruct only, optional |
|
|
| generated code exemption | architecture rule에 package/path 명시 필수 |
|
|
| Jackson deserialization defaults (B1) | `FAIL_ON_UNKNOWN_PROPERTIES=true` 명시 (`spring.jackson.deserialization.fail-on-unknown-properties=true`), `FAIL_ON_NULL_FOR_PRIMITIVES=true` 또는 wrapper type only, `READ_UNKNOWN_ENUM_VALUES_AS_NULL=false` 유지 |
|
|
| polymorphic deserialization (B5) | `@JsonTypeInfo(use = NAME)` + `@JsonSubTypes` 명시 또는 `BasicPolymorphicTypeValidator` allowlist 만 허용 |
|
|
| PATCH semantics (B2) | absent / null / 값 3-상태 구분; `JsonNullable` 또는 `Optional<T>` wrapper 사용; RFC 7396 미채택 |
|
|
| outbound ACL mapper (B7) | outbound HTTP adapter 의 응답 → domain 변환에도 동일한 normalization / masking / public field selection 책임 적용 |
|
|
| bulk partial success (B8) | `success: false` + `error.code = BATCH_PARTIAL_FAILURE` + `error.details[]` 항목별 결과 배열 shape |
|
|
| virtual thread context (B6) | filter/interceptor 는 SLF4J 2.0+ MDC + `RequestContextHolder` 만 사용; `InheritableThreadLocal` 금지 |
|
|
| forbidden | reflection-based implicit mapping, entity/domain direct response serialization, `enableDefaultTyping()` / `LaissezFaireSubTypeValidator`, 클래스 단위 `@JsonIgnoreProperties(ignoreUnknown=true)`, `InheritableThreadLocal` |
|
|
|
|
## 결정-근거 매핑
|
|
|
|
> 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. company-tech-blog 출처는 `company-case-study` 로 라벨링하며 공식 best practice 로 격상하지 않는다.
|
|
|
|
| Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk |
|
|
|---|---|---|---|---|
|
|
| D1 | 모든 경계에 validation/mapping 책임을 둠 (2026-05-21) | UNSUPPORTED_DECISION (Clean Architecture / Hexagonal boundary 책임 원칙은 일반 design wisdom 이지만 본 branch 가 cite 한 sources — Stripe/Toss/RFC 7807/Spring/Google/JSON:API/GraphQL/GitHub — 중 normative 진술 없음) | N/A | DDD boundary / Hexagonal port-adapter 패턴의 raw 인용 (예: Vaughn Vernon, Reflectoring) 별도 보강 필요 |
|
|
| D2 | validation 책임 분리 — syntax, policy, invariant, persistence integrity (4-layer) | **MECHANISM SUPPORTED, TAXONOMY UNSUPPORTED_DECISION.** `raw/official-docs/validation-jakarta-bean-validation-3.0-spec.md#JBV-3.0-C1` (class-level constraint = validates state of class = multi-property → invariant mechanism), `#JBV-3.0-C2` (ConstraintValidator receives class instance → 여러 field 동시 접근 가능), `#JBV-3.0-C3` (group sequence short-circuit → syntax 선 실행 후 invariant 실행 패턴의 normative 근거). **4-layer 이름(syntax/policy/invariant/persistence integrity) 자체는 ca-tmpl internal decision — JBV spec 은 이 taxonomy 를 정의하지 않음.** | `official-standard` (mechanism) + UNSUPPORTED_DECISION (taxonomy naming + layer assignment) | sibling branch 와 4-layer 정의의 정합성 cross-review 필수. JBV-3.0-C1~C3 은 Bean Validation 이 syntax/invariant 구분 *없이* 실행됨을 보여줌 — 분리를 강제하는 것은 application 설계 결정임을 명시 필요 |
|
|
| D3 | mapper 는 변환뿐 아니라 normalization, masking, public field selection 의 경계 | `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" — masking 의도와 정합), `raw/official-docs/google-api-error-format.md#GOOG-ERR-C2` ("`message` is a developer-facing ... debug message" — public field 분리 사상) | `official-standard + official-vendor-doc` | mapper = security boundary 라는 강한 정의 자체는 cited sources 가 직접 권고하지 않음 — 일반 보안 원칙 (OWASP) 별도 raw 보강 권장 |
|
|
| D4 | mapper 도구 기본값 — 수기 mapper + record canonical constructor; MapStruct optional (사용 시 architecture exemption + contract test 필요) | UNSUPPORTED_DECISION (project-internal tool selection; cited sources 중 mapper 도구 선택 관련 normative / vendor 진술 없음) | N/A | 수기 mapper 의 boilerplate 비용 vs MapStruct generated 코드의 architecture leak 위험 trade-off 는 별도 측정 / vendor 비교 필요 |
|
|
| D5 | error envelope shape — custom 채택, RFC 7807 ProblemDetail 명시적 거부 (sibling branch `feature-business-rule-validation-contract` 와 동일 결정 공유) | `raw/official-docs/problem-detail-rfc-7807.md#RFC7807-C1` (canonical model `application/problem+json`), `#RFC7807-C2` (`type` URI primary identifier), `#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` 구현 — envelope 와 충돌), `raw/official-docs/spring-mvc-rest-exception-handling.md#SPRING-MVC-EXC-C1` (동일 사실의 primary source — 모든 Spring MVC 내장 예외는 `ErrorResponse` 구현), `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}` 평면) | `official-standard + official-vendor-doc + company-case-study` | sibling branch D5 와 동일 evidence — cross-branch 일관성 확보됨. `SPRING-MVC-EXC-C1` 이 `SPRING-PD-C2` 를 corroborate. 단 RFC 7807 미채택 trade-off 의 ca-tmpl 측 해석은 cited sources 가 직접 권고하지 않음 |
|
|
| D10 | **B3 블라인드 해소** — `HttpMessageNotReadableException` (JSON 역직렬화 실패) 은 `VALIDATION` 카테고리로 분류; `MethodArgumentNotValidException` (Bean Validation 실패) 은 `VALIDATION` 카테고리로 분류; mapper 내부 예외 (`IllegalArgumentException`, MapStruct NPE, record canonical constructor `IllegalStateException`) 는 Spring 이 자동 처리하지 않으므로 별도 `@ExceptionHandler` 로 처리하며 ca-tmpl operational contract 에서 `MAPPING_FAILED` 카테고리로 분류 | `raw/official-docs/spring-mvc-rest-exception-handling.md#SPRING-MVC-EXC-C1` (모든 Spring MVC 내장 예외는 `ErrorResponse` 구현 — `HttpMessageNotReadableException` 포함), `#SPRING-MVC-EXC-C2` (`ResponseEntityExceptionHandler` 가 모든 Spring MVC 내장 예외 + `ErrorResponseException` 처리), `#SPRING-MVC-EXC-C4` (`HttpMessageNotReadableException` 은 normative 처리 목록에 있음), `#SPRING-MVC-EXC-C5` (`MethodArgumentNotValidException` 은 normative 처리 목록에 있음, {0}=global errors, {1}=field errors); `raw/official-docs/validation-jakarta-bean-validation-3.0-spec.md#JBV-3.0-C5` (PARAMETER ElementType → Bean Validation 으로 method parameter 검증 → `MethodArgumentNotValidException` 발생 경로의 2차 normative 확인); mapper-internal 예외의 `MAPPING_FAILED` 카테고리 코드 자체는 UNSUPPORTED_DECISION — ca-tmpl 고유 operational contract | `official-vendor-doc` (`HttpMessageNotReadableException` / `MethodArgumentNotValidException` → Spring normative) + `official-standard` (JBV-3.0-C5 corroboration) + UNSUPPORTED (`MAPPING_FAILED` 카테고리 코드 및 mapper-internal 예외 분류) | Spring 이 `HttpMessageNotReadableException` 의 HTTP status 를 400 으로 설정한다는 것은 `spring-mvc-rest-exception-handling.md` 의 message code 표에서 직접 명시되지 않음 — `ErrorResponse` 구현체 내부(Spring source)에서 정의됨. `MAPPING_FAILED` 라는 category code 를 Operational Error Category 에 추가하는 결정은 ca-tmpl 내부 결정이며 별도 project-note 갱신 필요 |
|
|
| D6 | retryable 1급 + success flag — 어떤 표준에도 1:1 매칭 없음 (sibling branch D6 와 동일) | `raw/official-docs/google-api-error-format.md#GOOG-ERR-C3` (typed details 다형성), `#GOOG-ERR-C5` (표준 detail payloads), `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` | sibling branch 와 동일 evidence; top-level 1급 retryable 은 ca-tmpl 고유 결정 |
|
|
| D7 | request DTO → application command/query mapper 강제 (DTO 의 service 직접 전달 금지); PATCH 요청 시 mapper 가 null vs absent 를 구분해야 함 (B2 블라인드) | `raw/official-docs/patch-json-merge-rfc7396.md#RFC7396-C2` ("Null values in the merge patch are given special meaning to indicate the removal of existing values in the target." — null=deletion normative), `#RFC7396-C3` (merge patch 는 explicit null 사용 시 부적합), `#RFC7396-C4` (배열 부분 수정 불가 — merge patch 한계) | `official-standard` (null=deletion 근거) + UNSUPPORTED (Hexagonal boundary 원칙 자체) | RFC7396 은 null=deletion 의 normative 근거를 제공하나, Java record mapper 에서 absent field 를 별도 처리하는 구현 방법은 직접 권고하지 않음. Hexagonal port-adapter boundary 원칙 raw (예: Reflectoring, Woowahan) 별도 인용 보강 권장 |
|
|
| D8 | domain object → response DTO 직접 노출 금지; response mapper 가 public field 만 선택 | `raw/official-docs/problem-detail-rfc-7807.md#RFC7807-C5` (detail 은 client correct 목적 — debug 정보 분리), `raw/official-docs/json-api-errors-spec.md#JSONAPI-ERR-C5` (`title` 은 호출별 불변 — public field 안정성 사상) | `official-standard` | response mapper 의 public field selection 강제 메커니즘 자체는 일반 design 원칙 — `@JsonView` / DTO record 같은 구체적 구현 표준 없음 |
|
|
| D9 | filter/interceptor 가 request context propagation 담당 | UNSUPPORTED_DECISION (project-internal middleware 결정; 외부 표준 근거 없음) | N/A | Servlet filter chain 의 ordering / context propagation 보장은 별도 ArchUnit / integration test 필요 |
|
|
| D11 | **B1 블라인드 해소** — Jackson deserialization 정책: `FAIL_ON_UNKNOWN_PROPERTIES=true` 명시, request DTO 는 wrapper type 또는 `FAIL_ON_NULL_FOR_PRIMITIVES=true`, 클래스 단위 `@JsonIgnoreProperties(ignoreUnknown=true)` ArchUnit 금지 | `raw/official-docs/schema-jackson-unknown-field-handling.md#SJUF-C1` (Jackson 2.13+ default — unknown property → `JsonMappingException`), `#SJUF-C2` (`FAIL_ON_NULL_FOR_PRIMITIVES=false` default → JSON null → 0 silently — ca-tmpl 의 null/empty/missing 분리와 불일치), `#SJUF-C3` (`FAIL_ON_IGNORED_PROPERTIES=false` default — silently skip), `#SJUF-C4` (`READ_UNKNOWN_ENUM_VALUES_AS_NULL=false` default — exception throw) | `official-vendor-doc` | Spring Boot `JacksonProperties` 가 default 를 override 하지 않는다는 보장은 별도 — `application.yaml` 명시 설정 검증 필요. `@JsonIgnoreProperties(ignoreUnknown=true)` 클래스 단위 사용 금지를 강제하는 ArchUnit rule 자체는 project-internal |
|
|
| D12 | **B5 블라인드 해소** — sealed `Command` interface + record subtypes 의 Jackson polymorphic deserialization: `ObjectMapper.enableDefaultTyping()` / `activateDefaultTyping(LaissezFaireSubTypeValidator)` 금지, `@JsonTypeInfo(use = NAME)` + `@JsonSubTypes` 명시 또는 `BasicPolymorphicTypeValidator` allowlist | `raw/official-docs/schema-jackson-polymorphic-deserialization.md#JACK-POLY-C1` (`PolymorphicTypeValidator` = default typing + `@JsonTypeInfo` class-name 기반 subtype 검증 공식 인터페이스, `@since 2.10`), `#JACK-POLY-C2` ("pluggable allow lists to avoid security problems that occur with unlimited class names"), `#JACK-POLY-C3` (`enableDefaultTyping()` = 2.10 `@Deprecated`, 대체 `activateDefaultTyping(PolymorphicTypeValidator)`), `#JACK-POLY-C4` (`BasicPolymorphicTypeValidator` = class hierarchy/name pattern allowlist 표준 구현체), `#JACK-POLY-C5` (NVD CVE-2019-14379: default typing + ehcache gadget → RCE, CVSS 9.8) | `official-vendor-doc + official-standard` | ArchUnit 으로 `enableDefaultTyping()` import / 호출 금지를 강제하는 rule 자체는 project-internal. sealed interface 패턴 사용 시 Jackson 의 sealed type 자동 인식 (Jackson 2.15+) 적용 여부는 별도 확인 필요 |
|
|
| D13 | **B6 블라인드 해소** — Virtual thread (`spring.threads.virtual.enabled=true`) 활성화 시 filter/interceptor `ThreadLocal` context propagation 안전성 contract test 강제, MDC 는 SLF4J 2.0+ 위임, `InheritableThreadLocal` 금지 | `raw/official-docs/runtime-spring-boot-virtual-threads.md#SPRING-VT-C1` (virtual thread 활성화 시 task executor 는 `SimpleAsyncTaskExecutor` 로 전환), `#SPRING-VT-C2` (비활성화 시 `ThreadPoolTaskExecutor`), `#SPRING-VT-C3` (scheduler 는 `SimpleAsyncTaskScheduler` 로 전환, pooling 속성 무시), `#SPRING-VT-C4` (builder bean 도 virtual thread 조건 충족 시 auto-config) | `official-vendor-doc` (executor/scheduler 전환) + UNSUPPORTED_DECISION (Tomcat connector 전환 + `RequestContextHolder` / MDC virtual-thread 호환성) | Spring Boot reference 의 task-execution 페이지는 executor/scheduler 전환만 명시. Tomcat embedded connector 의 virtual thread 적용 여부, `RequestContextHolder` 의 virtual thread 호환성, MDC 의 Loom 호환성은 별도 raw (Tomcat docs / SLF4J 2.0 docs / JEP 444) 보강 필요 |
|
|
| D14 | **B8 블라인드 해소** — Bulk endpoint partial success: envelope `success` flag = 전체 성공 시에만 true, 부분 실패는 `success: false` + `error.code = BATCH_PARTIAL_FAILURE` + `error.details[]` 항목별 결과 배열, OpenAPI 에서 별도 response shape 분기 | `raw/official-docs/google-api-error-format.md#GOOG-ERR-C3` (typed details 다형성 — item별 결과 표현 모델), `#GOOG-ERR-C5` (표준 detail payloads 카탈로그), `raw/official-docs/json-api-errors-spec.md#JSONAPI-ERR-C1` (errors array 다중 표현 — 적용 시), `raw/official-docs/graphql-errors-spec.md#GQL-ERR-C3` (partial response — data+errors 공존 — 분리 envelope 의 영감) | `official-vendor-doc + official-standard` (선례 다형성 / partial response 패턴) + UNSUPPORTED_DECISION (`BATCH_PARTIAL_FAILURE` code 명명 자체는 ca-tmpl 고유) | `BATCH_PARTIAL_FAILURE` code 를 Operational Error Category (canonical SSOT §6) 에 신규 등록 필요. 별도 `BatchResult<T>` envelope 도입 대안은 ca-tmpl `success/error 대칭` 정책과 충돌 위험 — 측정/리뷰 후 결정 |
|
|
| D15 | **B9 cross-cite** — Resource identifier ArchUnit rules **4개** (`no_long_id_pk` — `..domain..` 한정, `no_uuid_random_in_controller`, `no_math_random_for_id`, `no_varchar_255_for_id_column`) 를 본 branch ArchUnit suite 에 등록. 구현 skeleton 은 resource-identifier branch §구현 가이드 §6. **5번째 rule `no_find_by_id_without_tenant` 는 `feature-tenant-context-policy` (예정 branch) 로 이관** — tenant 모델 부재 시 production code 가 모두 깨지는 false positive 차단 | [[raw/branch-notes/feature-resource-identifier-contract]] D17 (rule SSOT — 4 rules), D5 (ID generation = domain port + application 주입), D10 (PostgreSQL `uuid` native), D13 (ID 내 tenant 인코딩 거부 — 형식적 위치만). project §34 Stack Commitment (archunit-junit5 1.3.0) | `cross-branch-SSOT` (resource-identifier D17) + `project-ssot` (§34 archunit-junit5 version) | `haveExplicitColumnLength()` custom ArchCondition 의 archunit-junit5 1.3.0 API 호환성 검증 필요 (resource-identifier branch §구현 가이드 §6 UNSUPPORTED_IMPL_DECISION). 본 4개 rule 의 실제 코드는 boundary branch ArchUnit suite 가 호스팅, *결정 SSOT* 는 resource-identifier branch D17. `no_find_by_id_without_tenant` 활성화는 multi-tenancy-contract 도착 시 |
|
|
|
|
## 검증해야 할 주장
|
|
|
|
> 공식 문서/사례는 근거지만 내 프로젝트에서의 동작을 자동 보장하지 않는다. 구현 전/중/후 실제로 검증해야 하는 주장.
|
|
|
|
| Claim | Why uncertain | How to verify | Status |
|
|
|---|---|---|---|
|
|
| controller 가 domain object 를 직접 반환하지 않는지 (response mapper boundary 강제) | implicit Jackson serialization 으로 domain object 가 직접 직렬화될 위험 | ArchUnit rule (controller method return type 은 DTO/record/`ResponseEntity<DTO>` 만) + integration test | `partially-implemented` (2026-05-29 3차 패스: `EnvelopeBodyAdvice` 가 모든 컨트롤러 응답을 `Envelope<T>` 로 wrap, 기존 컨트롤러는 DTO record 만 반환 컨벤션. 컨트롤러 반환 타입의 정적 ArchUnit rule 은 미작성 — `planned` 잔존.) |
|
|
| request DTO 가 application service 의 method signature 에 직접 나타나지 않는지 | DTO 가 service layer 까지 leak 가능성 | ArchUnit rule (`service` package method 의 parameter type 은 `Command`/`Query` record 만) | `planned` |
|
|
| MapStruct generated code 가 architecture exemption 없이 architecture rule 우회하지 않는지 | `target/generated-sources` 의 generated mapper 가 domain access 시 silent rule bypass | ArchUnit rule 의 generated code exemption package 명시 + generated code 의 domain access pattern 검증 | `planned` |
|
|
| filter/interceptor 가 request context (traceId, principal, tenant) 를 application layer 까지 propagate 하는지 | Spring `RequestContextHolder` 또는 MDC propagation 누락 가능 | integration test (downstream service 에서 context 값 접근 가능 검증) + `@Async` boundary test | `planned` |
|
|
| mapper 가 PII / sensitive field 를 mask 하는지 (e.g., 카드번호, 주민번호, 이메일) | mapper 가 단순 변환만 하고 masking 누락 가능 | DLP scan + 의도적 PII field test (response body grep) | `planned` |
|
|
| MapStruct 사용 시 generated code 가 architecture exemption package 에 격리되는지 | exemption 없이 사용 시 ArchUnit rule 우회 | build 시 generated code path 검증 + ArchUnit rule 의 exemption 명시 확인 | `needs-confirmation` |
|
|
| internal diagnostic context (debug info, stacktrace, internal IDs) 가 response payload 에 섞이지 않는지 | exception handler 또는 mapper 에서 internal context 누출 가능 | response leakage contract test (debug field regex grep) + production log audit | `planned` |
|
|
| RFC 7807 미채택이 Spring 6 의 autoconfigure (`spring.mvc.problemdetails.enabled`, `SPRING-PD-C4`) 와 충돌하지 않는지 (sibling branch D5 와 동일 우려) | sibling branch business-rule-validation 과 동일한 risk | `spring.mvc.problemdetails.enabled=false` 명시 설정 검증 + Spring MVC error response shape contract test | `actually-implemented` (2026-05-29 3차 패스: `GlobalExceptionHandler` 가 `ProblemDetail` import 완전 제거 + `Envelope<Void>` 반환, `BoundaryDemoControllerWireTest` 의 11 케이스가 envelope shape 을 wire-level 로 pin. Spring 의 `ProblemDetail` 자동 활성화도 우리 핸들러가 우선이므로 충돌 없음.) |
|
|
| (B1) `spring.jackson.deserialization.fail-on-unknown-properties=true` 가 실제 설정되어 unknown field 가 400 으로 거부되는지 | Spring Boot `JacksonProperties` 가 Jackson default 를 silent override 가능 | `application.yaml` 명시 검증 + integration test (unknown field 가 포함된 JSON 요청 → 400 응답 + `VALIDATION_FAILED` code) | unknown-field 거부와 4종 binding 설정은 기존 테스트 기록이 있으나 당시 envelope 기대값이 제거된 `MALFORMED_REQUEST`였다. D10의 `VALIDATION_FAILED`로 갱신한 wire test 재실행 전까지 `needs-confirmation` |
|
|
| (B1) request DTO 중 primitive type 이 있는지 (있다면 `FAIL_ON_NULL_FOR_PRIMITIVES=true` 또는 wrapper 전환 필요) | Jackson default 는 JSON null → primitive 0 silently | ArchUnit rule (request DTO record 의 component type 은 wrapper 또는 `Optional` 만) + Jackson configuration test | `planned` (스위치는 `locally-verified`. component-type ArchUnit rule 은 미작성.) |
|
|
| (B1) 클래스 단위 `@JsonIgnoreProperties(ignoreUnknown=true)` 사용 여부 | 정책 우회 risk | ArchUnit rule (request DTO 패키지 내 `@JsonIgnoreProperties` 사용 금지) | `actually-implemented` (2026-05-29: `request_dtos_do_not_silence_unknown_fields` + `JsonIgnoreUnknownRequestFixture` 위반-증명 테스트.) |
|
|
| (B2) PATCH endpoint 가 absent / null / 빈 값을 mapper 에서 구분하여 처리하는지 | record 기본값으로 mapping 시 PATCH 가 null 로 덮어쓰는 silent overwrite | PATCH integration test (3 케이스: field 없음 → 무변경, field=null → 명시적 null, field=value → 갱신) + JSON Schema validation | `actually-implemented` (2026-05-29 3차 패스: `BoundaryDemoControllerWireTest#b2_patch_field_absent_is_distinguished_from_explicit_null_and_value` 가 PATCH `/demo/boundary/patch-demo` 로 3 케이스 wire-level pin. 기존 `UpdateProfileRequest` + `UpdateProfileCommand` + `UserService.updateProfile` 도 `JsonNullable<T>` / `Patch<T>` 로 마이그레이션 — silent overwrite 위험 제거.) |
|
|
| (B2) RFC 7396 미채택 사실이 OpenAPI 문서에 명시되는지 (`application/merge-patch+json` content type 사용 안 함) | 클라이언트가 RFC 7396 semantics 를 가정할 risk | OpenAPI spec 검토 + content type assertion test | `planned` |
|
|
| (B3) mapper 내부 예외 (record canonical constructor `IllegalArgumentException`, MapStruct NPE) 가 별도 `@ExceptionHandler` 로 잡혀 `MAPPING_FAILED` 카테고리로 분류되는지 | Spring 이 자동 처리하지 않으므로 `INTERNAL` 로 새어 나가는 risk | controller advice integration test (의도적 mapper exception 발생 → `MAPPING_FAILED` 응답 검증) | `actually-implemented` (2026-05-29 3차 패스: `BoundaryDemoControllerWireTest#b3_mapping_exception_surfaces_as_mapping_failed_envelope` 가 POST `/demo/boundary/mapping-failure` 로 advice integration 검증. `GlobalExceptionHandlerTest` 가 unit 레벨 + envelope shape pin.) |
|
|
| (B3) `MAPPING_FAILED` 신규 code 가 ca-tmpl operational contract canonical SSOT §6 에 등록되었는지 | code 누락 시 sibling branch error envelope 와 정합 깨짐 | [[raw/project-notes/ca-skeleton-operational-contract]] §6 갱신 PR 검증 | `actually-implemented` (2026-05-29 §6 등록 완료) |
|
|
| (B4) request DTO 에 `@GroupSequence` 로 syntax → invariant short-circuit 패턴 적용되는지 | Bean Validation default 는 모든 group 평탄 실행 — invariant 가 syntax 실패 후에도 평가됨 | Bean Validation integration test (의도적 syntax 실패 → invariant validator 호출되지 않음 검증) | `actually-implemented` (2026-05-29: `SampleGroupSequenceRequest` + `SampleGroupSequenceRequestTest`. invariant 메서드가 null 필드와 만나면 `IllegalStateException` 을 던지도록 만들어 short-circuit 회귀 시 테스트가 빨갛게 떨어진다.) |
|
|
| (B4) `@Valid` cascade depth 가 3 단계 이내인지 (DoS 방어) | nested 객체 deep recursion 시 CPU 소모 | ArchUnit rule (nested `@Valid` annotation depth scan) + load test | `planned` (현 패스에서 nested DTO sample 부재로 ArchUnit 동적 검사 미작성 — cascade depth 컨벤션은 `adapter-web/CLAUDE.md` 에 문서화.) |
|
|
| (B5) `ObjectMapper.enableDefaultTyping()` / `activateDefaultTyping(LaissezFaireSubTypeValidator)` 호출이 코드 어디에도 없는지 | CVE-2019-14379 류 gadget chain RCE risk | ArchUnit rule (`enableDefaultTyping` / `LaissezFaireSubTypeValidator` import 금지) + dependency check (jackson-databind 버전 최소 2.10+) | `actually-implemented` (2026-05-29: `no_jackson_laissez_faire_subtype_validator` + `no_jackson_enable_default_typing_call` 두 ArchUnit rule, `DefaultTypingFixture` 가 violations-as-data 로 catch 검증. jackson-databind 버전 확인은 별도 supply-chain branch 책임.) |
|
|
| (B5) sealed `Command` interface 가 `@JsonTypeInfo` + `@JsonSubTypes` 명시 또는 `BasicPolymorphicTypeValidator` allowlist 로만 deserialize 되는지 | 명시 누락 시 sealed type 도 deserialize 불가 | polymorphic deserialization integration test (각 subtype 정상 deserialize + allowlist 외 type 거부) | `actually-implemented` (2026-05-29: `SamplePolymorphicRequest` sealed interface + record subtypes + `@JsonTypeInfo`/`@JsonSubTypes` + `SamplePolymorphicRequestTest` 4 케이스. allowlist 외 discriminator → `InvalidTypeIdException` pin.) |
|
|
| (B6) `spring.threads.virtual.enabled=true` 환경에서 filter/interceptor 의 `RequestContextHolder` + MDC propagation 이 application layer 까지 도달하는지 | Loom virtual thread 의 `ThreadLocal` semantics 미검증 | `@SpringBootTest(properties = "spring.threads.virtual.enabled=true")` integration test (downstream service 에서 `requestId` / `traceId` / `MDC.get()` 접근 가능 검증) | `actually-implemented` (2026-05-29 3차 패스: `VirtualThreadMdcE2ETest` 가 `@SpringBootTest(RANDOM_PORT)` + 가상 스레드 + 실 `RequestLoggingFilter` + `TestRestTemplate` 로 server-generated `requestId` 와 client-supplied `X-Request-Id` 두 경로 모두 컨트롤러까지 도달함을 wire-level 로 pin.) |
|
|
| (B6) `InheritableThreadLocal` 직접 사용이 없는지 + MDC 가 SLF4J 2.0+ 사용하는지 | virtual thread 환경에서 `InheritableThreadLocal` 누설 가능 | ArchUnit rule (`InheritableThreadLocal` import 금지) + SLF4J 버전 dependency check | `actually-implemented` (2026-05-29: `no_inheritable_thread_local` rule + `InheritableThreadLocalFixture` 위반 catch 검증. SLF4J 2.0+ 버전 확인은 별도 supply-chain branch.) |
|
|
| (B7) outbound HTTP adapter 의 응답 → domain 변환 mapper 가 ACL 책임 (normalization / masking / public field selection) 을 inbound mapper 와 동일하게 적용하는지 | outbound 응답이 domain 으로 raw leak 가능 | ArchUnit rule (outbound adapter `RestClient` / `WebClient` 반환 타입 = ACL mapper 통과 후 domain type 만) + integration test (외부 응답 raw 가 domain object 에 그대로 leak 되지 않음) | `actually-implemented` (2026-05-29: `WeatherSummary` (domain) + `WeatherForecastPort` (application) + `RawWeatherResponse` (package-private, adapter-only) + `WeatherForecastAclMapper` + `WeatherForecastClient` + `WeatherForecastClientTest` 3 케이스. 부수 효과로 `outbound_adapter_does_not_depend_on_web_or_persistence_adapters` ArchUnit 규칙이 `MappingException` 의 잘못된 위치를 catch — `application.exception` 으로 이전.) |
|
|
| (B8) Bulk endpoint 의 response 가 `success: false` + `error.code = BATCH_PARTIAL_FAILURE` + `error.details[]` 항목별 결과 shape 을 따르는지 | 단일 항목 endpoint 와 schema 혼동 risk | bulk endpoint contract test (전체 성공 / 전체 실패 / 부분 실패 3 케이스) + OpenAPI shape 분기 검증 | `actually-implemented` (2026-05-29 3차 패스: `BoundaryDemoControllerWireTest` 의 3 bulk 케이스 (`b8_all_success`, `b8_partial_failure`, `b8_all_failures_take_the_same_partial_branch`) 가 POST `/demo/boundary/bulk` 로 wire-level 검증. OpenAPI 분기는 스펙 자체가 부재라 별도.) |
|
|
| (B8) `BATCH_PARTIAL_FAILURE` 신규 code 가 canonical SSOT §6 에 등록되었는지 | code 누락 시 envelope 일관성 깨짐 | [[raw/project-notes/ca-skeleton-operational-contract]] §6 갱신 PR 검증 | `actually-implemented` (2026-05-29 §6 등록 완료) |
|
|
|
|
## 구현 결과
|
|
|
|
> 후속 audit (`<project>/docs/superpowers/specs/2026-05-29-module-placement-audit-report.md`) 에서 **이전 4개 브랜치가 만든 skeleton-wide 운영 계약이 sample-portfolio 에만 구현되어 실행 앱(app-bootstrap)에서 누락**되는 High 결함(Finding 1)을 발견. app-bootstrap 은 sample-portfolio 을 런타임 의존하지 않으므로(`testImplementation` only) fork 후 sample 삭제 시 envelope/error 계약이 통째로 사라짐. 이를 production 모듈로 승격하는 리팩터를 TDD + subagent-driven 으로 수행.
|
|
|
|
### 승격 내역 (동작 보존, 패키지/모듈 이동 중심)
|
|
|
|
- **shared-contract (stdlib-only)**: `error/ApiErrorCode` 인터페이스 신설(code/httpStatus(int)/retryable — Spring `HttpStatus` 대신 전송중립 int 로 stdlib 제약 충족) + `error/OperationalError` enum(운영/전송/보안 코드) + `error/MappingException` 이전 + `response/BulkEnvelope`·`BulkItemResult` 이전(`OperationalError.BATCH_PARTIAL_FAILURE` 사용).
|
|
- **adapter-web**: `error/GlobalExceptionHandler` (base @RestControllerAdvice, 운영/전송/보안/framework 예외만) + `error/ErrorResponseFactory` (int→`HttpStatus.valueOf` + MDC traceId, envelope 빌드 단일 지점) + `envelope/EnvelopeBodyAdvice` 이전 + `config/JacksonNullableConfig` 이전(+`jackson-databind-nullable` 의존).
|
|
- **sample-portfolio**: `ApiErrorCode` enum → `SampleErrorCode`(도메인 코드만, shared 인터페이스 구현) + `DomainExceptionHandler`(도메인 예외 전용 advice, base 와 Spring 합성). 기존 단일 `GlobalExceptionHandler`(운영+도메인 혼재) 삭제.
|
|
- **app-bootstrap**: 코드 변경 0. `OperationalContractRuntimeTest`(@WebMvcTest, `CaSkeletonApplication` 앵커 + raw probe) 신설 — 실행 컨텍스트에 advice/handler 빈 존재 + raw body 가 실제로 wrap 됨을 pin → Finding 1 회귀 방지.
|
|
- **문서**: `shared-contract/CLAUDE.md` 신설(부재했음), `adapter-web/CLAUDE.md` 의 "handler 가 sample 에 있다" 구절을 "production 모듈로 승격됨"으로 갱신.
|
|
|
|
### 검증
|
|
|
|
- 9 Task TDD, task 마다 `./gradlew test verifyCleanArchitectureDependencies` green. 최종 `./gradlew clean test verifyCleanArchitectureDependencies` → BUILD SUCCESSFUL, **119 tests / 0 failures**.
|
|
- ArchUnit 24 규칙 + violation fixture 전부 green. shared-contract Spring/Jackson/JPA import 0 (grep 확인). `production_code_does_not_depend_on_sample_portfolio` green.
|
|
- 최종 리뷰: ca-architect-sentinel **PASS**, ca-quality-reviewer 의 Important 2건(BulkEnvelope double-wrap 분기 미테스트 / DomainExceptionHandler 라우팅 미테스트) 보강 테스트 추가 후 green, minor(stale Javadoc, `.toList()` 일관화, dead `INTEGRITY_VIOLATION` 제거, inline FQN→import) 처리.
|
|
|
|
### 잔여 / 후속
|
|
|
|
- 커밋은 사용자가 일괄 수행 예정(현재 working tree 미커밋). 본 5차 패스는 `feature/boundary-validation-mapping-contract` 브랜치 작업 트리에 존재.
|
|
- sub-project B: sample 도메인을 포트폴리오 게시판으로 교체 + 모듈 rename — 별도 spec/plan 예정.
|
|
- 설계/계획 문서: `<project>/docs/superpowers/specs/2026-05-29-operational-contract-promotion-design.md`, `<project>/docs/superpowers/plans/2026-05-29-operational-contract-promotion.md` (repo `/docs` gitignore 로 untracked).
|
|
|
|
## 구현 결과
|
|
|
|
> 감사 Finding 4(sample 도메인·데모 비일관) 해소. sample 모듈을 사용자의 엔지니어링 작업물을 보여주는 **포트폴리오 게시판(WorkLog)** 으로 교체하고, production 모듈 경계를 거울처럼 보여주는 adapter-mirrored 레이아웃으로 정리. production 모듈·ArchUnit 본체는 불변(glob/매트릭스 키만 rename).
|
|
|
|
### Phase B-1 — rename + restructure (동작 보존)
|
|
- `sample-portfolio` → `sample-portfolio`, 패키지 `dev.caskeleton.sample.portfolio` → `dev.caskeleton.sample.portfolio`. settings.gradle / `verifyCleanArchitectureDependencies` 매트릭스 키 / app-bootstrap `testImplementation` / ArchUnit `production_code_does_not_depend_on_sample_portfolio` glob(`..sample.portfolio..`→`..sample.portfolio..`) 전부 갱신. (glob 미갱신 시 vacuous-pass → production→sample 미탐지, 계약 보존 필수 포인트.)
|
|
- 절반-마이그레이션 빈 `.gitkeep` anchor(domain/model, application/usecase/port/in 등) 제거. 모듈 CLAUDE.md(adapter-web/app-bootstrap/domain-core)의 stale `com.example.blog.*` → `dev.caskeleton.*` 교정.
|
|
|
|
### Phase B-2 — WorkLog 도메인 (adapter-mirrored)
|
|
- domain/worklog: `WorkLog`(POJO 엔티티), `WorkCategory`(INFRASTRUCTURE/DATABASE/BACKEND/PLATFORM), `Period`(vo), `RepoStats`(vo), `WorkLogRepository`(port).
|
|
- application/worklog: `Create/Update/Delete/Get/ListWorkLogsUseCase` + `GetRepoStatsUseCase` — **sample에서 처음으로 application-port-usecase 계약 실증**(`CommandUseCase`/`QueryUseCase` + `@UseCaseCapability` + `TransactionPort`, `@Transactional` 미사용). command/query/exception 분리.
|
|
- adapter/web: `WorkLogController`(목록=메인화면 + CRUD + bulk import + repo-stats), DTO(B4 `@GroupSequence`, B2 `JsonNullable→Patch`, B1 unknown-field), `WorkLogWebMapper`(B3 `MappingException`), `PortfolioErrorCode`, `DomainExceptionHandler`(`@Order(HIGHEST_PRECEDENCE)` — base catch-all보다 앞서야 도메인 예외가 INTERNAL로 안 빨려듦).
|
|
- adapter/persistence: `WorkLogEntity`(@ElementCollection LAZY), `WorkLogJpaRepository`, `WorkLogRepositoryAdapter`(page 기반), `WorkLogPersistenceMapper`.
|
|
- adapter/outbound/repostats: B7 ACL(`RawRepoStatsResponse` package-private + `RepoStatsAclMapper` normalization/masking + `RepoStatsPortClient`) — weather 대체, `GetRepoStatsUseCase`로 실제 소비(orphan 아님).
|
|
- B1/B2/B3/B4/B8 계약을 WorkLog 엔드포인트로 re-home, B5(polymorphic)는 `SamplePolymorphicRequestTest` 단위테스트로 유지, B6(virtual-thread MDC)는 self-contained probe로 재배치. User/Post/BoundaryDemo/weather 전체 제거.
|
|
- README(`src/sample-portfolio/README.md`) + 루트 README/CLAUDE.md/AGENTS.md의 `sample-portfolio`→`sample-portfolio` 갱신. 시드 2건(Keycloak+k3s+Vault 인증위임 / DB 쿼리튜닝)은 README curl 예시.
|
|
|
|
### 검증 / 리뷰
|
|
- subagent-driven 9 Task, 단계마다 green. 최종 `./gradlew clean test verifyCleanArchitectureDependencies` → BUILD SUCCESSFUL, **126 tests / 0 failures**. ArchUnit 24규칙 + 19 픽스처 green(use-case 규칙이 이제 WorkLog로 실제 검증).
|
|
- 부수 발견: `src/build.gradle`에 `-parameters` 컴파일 플래그 누락(Spring `@PathVariable`/`@RequestParam` 이름 해석 실패) → 프로젝트 전역 추가.
|
|
- 최종 리뷰: ca-architect-sentinel **PASS**(URI-check/bulk-branching은 boundary/transport, 위반 아님), ca-quality-reviewer Important 5건(findAll offset→page 버그, bulk catch granularity, findAll 테스트 공백, PATCH @Valid+explicit-null 미테스트+dead @Size, RepoStatsPort dead code) 보강 후 green.
|
|
|
|
### 잔여
|
|
- 커밋은 사용자가 A+B 일괄 수행 예정(working tree 미커밋).
|
|
- `@Version` 낙관적 락 / 실 WebClient+WireMock / @DataJpaTest 통합 / `@MockBean`→`@MockitoBean` 는 후속.
|
|
- 설계/계획: `<project>/docs/superpowers/specs/2026-05-29-sample-portfolio-domain-design.md`, `<project>/docs/superpowers/plans/2026-05-29-sample-portfolio-domain.md`.
|
|
|
|
## 엣지·실패·의존
|
|
|
|
> 본 branch 가 의존하거나 깨질 수 있는 경계 조건. 상세 검증 항목은 §Claims To Verify, 운영 영향은 §구현 가이드 §4 참조.
|
|
|
|
- **Edge**: PATCH 의 absent vs explicit-null vs value 3-state — 구분 실패 시 silent overwrite (B2). bulk endpoint 의 전체 실패도 부분 실패와 동일 `BATCH_PARTIAL_FAILURE`(HTTP 200) branch 를 타며, 분기는 `envelope.success` 로만 (B8).
|
|
- **Failure mode**: ① mapper-internal 예외가 `MappingException` wrap 누락 시 `INTERNAL_ERROR` 로 새어 분류 오류 (B3). ② virtual thread 환경에서 `ThreadLocal`/MDC context 가 application layer 까지 propagate 안 되면 traceId 유실 (B6). ③ ArchUnit 정적 강제는 바이트코드 carrier(어노테이션/import/호출)만 탐지 — 메서드 본문 free-form 문자열은 한계.
|
|
- **Dependency**: `MAPPING_FAILED` / `BATCH_PARTIAL_FAILURE` 신규 code 는 canonical SSOT [[raw/project-notes/ca-skeleton-operational-contract]] §6 등록에 의존 (등록 완료). package convention glob 은 §20 Skeleton Blueprint 에 의존. B9 ArchUnit rule 은 [[raw/branch-notes/feature-resource-identifier-contract]] D17 을 cross-cite.
|
|
|
|
## 관련 일일 노트
|
|
|
|
- (해당 enforcement 패스에서 단독 daily-note 추출 없음. 구현 진행은 §"구현 결과" 5/6차 패스 + §"마주친 문제" 에 직접 기록.)
|
|
|
|
## 마주친 문제
|
|
|
|
- 2026-05-29 (a): `JacksonDeserializationPolicyTest` 첫 컴파일 시 `com.fasterxml.jackson.databind.DeserializationFeature` 가 app-bootstrap 의 test classpath 에 없어 컴파일 실패. app-bootstrap 의 main `spring-boot-starter` 는 jackson 을 transitive 로 가져오지 않고, root `subprojects { ... testImplementation 'spring-boot-starter-test' }` 도 jackson-databind 를 guarantee 하지 않음. `testImplementation 'org.springframework.boot:spring-boot-starter-json'` 추가로 해소. 1회성 환경 정렬이므로 별도 `raw/errors/` 등재는 생략.
|
|
- 2026-05-29 (c): B8 응답 타입 promotion. `BulkEnvelope<T>` + `BulkItemResult` 를 `sample.portfolio.adapter.web.dto.response` 에서 stdlib-only `shared-contract` 의 `dev.caskeleton.shared.response` 로 이전 (기존 `Envelope` / `ApiError` 옆). `BulkEnvelope.partial(...)` 은 Task 1 에서 추가된 `dev.caskeleton.shared.error.OperationalError.BATCH_PARTIAL_FAILURE` 의 `.code()` / `.retryable()` 를 사용해 하드코딩 문자열을 제거. shared-contract 는 Spring/Jackson 미의존 — 두 타입 모두 `java.util.List` + 공유 `ApiError` 만 쓰는 plain record 라 제약 충족. 소비자 import 갱신: `EnvelopeBodyAdvice`, `BoundaryDemoController`, 그리고 same-package resolution 에 의존하던 `BulkEnvelopeTest` (명시 import 2 줄 추가). 동작 동일 — 패키지 이동만. 회귀 게이트: `./gradlew test verifyCleanArchitectureDependencies` → BUILD SUCCESSFUL, 112 tests / 0 failures / 0 errors (BulkEnvelopeTest 3 케이스 포함). 단순 이전이라 별도 `raw/errors/` 등재 불요.
|
|
- 2026-05-29 (b): B7 outbound ACL 참조 추가 후 `outbound_adapter_does_not_depend_on_web_or_persistence_adapters` ArchUnit 규칙이 fail. 원인: `MappingException` 이 `sample.portfolio.adapter.web.error` 패키지에 있어 `WeatherForecastAclMapper` (outbound) 가 web 에 의존하게 됨. **fitness function 이 dependency-direction 회귀를 정확히 catch 한 사례.** 해소: `MappingException` 을 `sample.portfolio.application.exception` 으로 이전 (다른 application exception 들과 같은 위치). adapter-web 의 `GlobalExceptionHandler` 와 outbound adapter 의 ACL mapper 모두 application 패키지에 의존하므로 의존성 방향이 다시 맞아 떨어진다.
|
|
|
|
## 완료 후 정리
|
|
|
|
- PR 링크:
|
|
- 리뷰 메모:
|
|
- 머지 결과 / 배포 환경:
|
|
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
|
|
- `actually-implemented` 항목:
|
|
- B1 정적 차단 (ArchUnit `request_dtos_do_not_silence_unknown_fields` + violation fixture) + wire-level (`BoundaryDemoControllerWireTest#b1_unknown_json_field_is_rejected_via_envelope`)
|
|
- B2 PATCH 3-state + wire-level (`BoundaryDemoControllerWireTest#b2_patch_field_absent_is_distinguished_from_explicit_null_and_value` 3 케이스) + 기존 `UpdateProfileRequest`/`UpdateProfileCommand`/`UserService` 마이그레이션 완료
|
|
- B3 `MappingException` → `MAPPING_FAILED` wire-level (`BoundaryDemoControllerWireTest#b3_mapping_exception_surfaces_as_mapping_failed_envelope` + unit)
|
|
- B4 `@GroupSequence` short-circuit + wire-level (`BoundaryDemoControllerWireTest` 의 b4 3 케이스)
|
|
- B5 Jackson default typing / `LaissezFaireSubTypeValidator` 차단 (ArchUnit + violation fixture, CVE-2019-14379 대응)
|
|
- B5 sealed type + `@JsonTypeInfo`/`@JsonSubTypes` 패턴 (unit `SamplePolymorphicRequestTest` + wire `BoundaryDemoControllerWireTest` 의 b5 2 케이스)
|
|
- B6 `InheritableThreadLocal` 차단 (ArchUnit + violation fixture)
|
|
- B6 virtual thread MDC propagation (`VirtualThreadMdcPropagationTest` unit + `VirtualThreadMdcE2ETest` 실 Tomcat + 실 가상스레드 + 실 `RequestLoggingFilter` 2 케이스)
|
|
- B7 outbound ACL mapper (Weather adapter + `WeatherForecastClientTest`)
|
|
- B8 bulk envelope (unit `BulkEnvelopeTest` 3 케이스 + wire `BoundaryDemoControllerWireTest` b8 3 케이스)
|
|
- **D5 RFC 7807 거부 완료**: `GlobalExceptionHandler` 가 `ProblemDetail` import 완전 제거 + `Envelope<Void>` 반환. shared-contract 의 skeleton-wide `Envelope<T>` / `ApiError` 타입 신설.
|
|
- **success/error 대칭**: `EnvelopeBodyAdvice` 가 모든 controller success 응답을 `Envelope.ok(...)` 로 자동 wrap.
|
|
- `locally-verified` 항목:
|
|
- B1 Jackson 4-종 deserialization 스위치 (`JacksonDeserializationPolicyTest`)
|
|
- `prod-verified` 항목: (해당 없음 — 본 패스는 enforcement + reference + unit/contract + wire-level + e2e 단계, prod 트래픽 검증 미수행)
|
|
- **추출하지 않을 항목** (planned / documented-only / abandoned):
|
|
- controller 반환 타입의 정적 ArchUnit rule — `planned` (현 패스는 `EnvelopeBodyAdvice` 자동 wrap 으로 우회).
|
|
- B4-2 `@Valid` cascade depth ≤ 3 동적 ArchUnit — `planned` (nested DTO sample 부재).
|
|
- B7-2 실 `WebClient`/`RestClient` + WireMock 통합 — `planned` (현 패스는 HTTP fetch 추상화).
|
|
- B8-2 OpenAPI shape 분기 명시 — `planned` (OpenAPI 스펙 부재).
|
|
- `MAPPING_FAILED` / `BATCH_PARTIAL_FAILURE` 의 canonical SSOT [[raw/project-notes/ca-skeleton-operational-contract]] §6 등재 (별도 envelope SSOT 갱신 PR 책임)
|