Files
llm-wiki/raw/branch-notes/feature-boundary-validation-mapping-contract.md

71 KiB

title, source_type, status, branch, parent_branch, related_projects, tags, created, last_reviewed, target_merge, status_label, last_implementation_pass, ingest_note, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, contract_packet_sha256
title source_type status branch parent_branch related_projects tags created last_reviewed target_merge status_label last_implementation_pass ingest_note id kind project work_item inherits refines overrides depends_on contract_packet contract_packet_sha256
branch / feature-boundary-validation-mapping-contract branch-note verified feature-boundary-validation-mapping-contract
ca-skeleton
branch
ca-skeleton
validation
mapper
boundary
2026-05-21 2026-06-04 in-progress 2026-05-29 (4th pass — Forbidden 정적 강제 + B5 sample 보강 + 문서 구현 가이드) 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. BR-CA-SKELETON-OPERATIONAL-CONTRACT-002 project-work-item ca-skeleton-operational-contract WI-CA-SKELETON-OPERATIONAL-CONTRACT-002
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MAPPING-001@1
1 4c15ac1bd65a18209652e97e9c30583cf8326a361f4979f7fc588e0ab66cb67a

branch: feature-boundary-validation-mapping-contract

Layer: raw/branch-notes/ — request/application/domain/response/filter 경계의 validation과 mapper 계약을 정의합니다.

부모 (필수)

ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다.

묶음

본 feature branch 는 leaf — 자식 자료 없음. Phase C2 실 코드 작성 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.

근거 자료

오류 기록 (본 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/ 등재 생략.)

면접 준비 (이 작업에서 나올 수 있는 면접 질문)

Blog topics (이 작업에서 파생)

브랜치 계약 패킷

  • 생성 시 프로젝트 개정: 1
  • 패킷 스키마: contract_packet: 1
  • 완료 조건: boundary·mapping 6필드 contract와 negative fixture가 통과한다

상속한 프로젝트 결정

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

브랜치 지역 결정

기존 branch-local 결정은 아래 ## Decision Evidence Map / 결정-근거 매핑의 D-row가 소유하며 이 packet에서 복제하지 않는다.

Decision ID Decision Relation Supporting Claims Status

선언한 예외

Override ID Overrides Reason Approval Status

목표

CA skeleton에서 경계가 흐려지면 DTO, domain object, persistence model이 서로 새어 나갑니다. 이 branch는 각 경계가 무엇을 검증하고 어떤 mapper를 통과해야 하는지 고정합니다.

  • 이슈:
  • PR:

범위

포함 범위

  • 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 / MethodArgumentNotValidExceptionVALIDATION 분류 근거
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_FAILEDD10 + SPRING-MVC-EXC-C1/C4/C5, JBV-3.0-C5
  • MAPPING_FAILEDD10 (canonical SSOT §6 등록 완료 2026-05-29)
  • BATCH_PARTIAL_FAILURED14 + 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_FAILURED14 + 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 해서 던진다. 그러면 handleMappingMAPPING_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: ①EnvelopeBodyAdviceSpring 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 의 BasicPolymorphicTypeValidatorAllowlistTest4-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-portfolioBasicPolymorphicTypeValidatorAllowlistTest 가 4 case 로 pin (allowlisted Cat/Dog 통과, 비허용 subtype 거부, 임의 JDK 클래스 거부).
  • 두 가지 정적 강제 + 두 가지 sample (sealed @JsonTypeInfo/@JsonSubTypesBasicPolymorphicTypeValidator) 모두 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 는 부분 normativeRFC7807-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..정확한 globraw/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_usageorg.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-C1SPRING-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_tenantfeature-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차 패스: GlobalExceptionHandlerProblemDetail 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.updateProfileJsonNullable<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-portfoliosample-portfolio, 패키지 dev.caskeleton.sample.portfoliodev.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 + GetRepoStatsUseCasesample에서 처음으로 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-portfoliosample-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> + BulkItemResultsample.portfolio.adapter.web.dto.response 에서 stdlib-only shared-contractdev.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. 원인: MappingExceptionsample.portfolio.adapter.web.error 패키지에 있어 WeatherForecastAclMapper (outbound) 가 web 에 의존하게 됨. fitness function 이 dependency-direction 회귀를 정확히 catch 한 사례. 해소: MappingExceptionsample.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 MappingExceptionMAPPING_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 거부 완료: GlobalExceptionHandlerProblemDetail 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 책임)