--- title: ca-tmpl - 입력 경계 검증 & DTO↔도메인 매핑 계약 (Bean Validation · Patch · ACL · 정적 강제) source_type: project status: verified confidence: high tags: [ca-tmpl, validation, mapper, boundary, dto, archunit, actually-implemented] related_projects: [ca-tmpl] last_reviewed: 2026-07-02 --- # ca-tmpl - 입력 경계 검증 & DTO↔도메인 매핑 계약 > Layer: `wiki/projects/` — 내 프로젝트 사실. 일반 개념은 [[wiki/concepts/boundary-validation-and-dto-mapping]] 참조. ## 프로젝트 컨텍스트 - **프로젝트**: ca-tmpl — Clean Architecture 기반 백엔드 skeleton 템플릿. - **목표**: request → application → response 의 입력/출력 경계에서 (1) 무엇을 검증하고 (2) 어떤 mapper 를 통과해야 하는지 고정하고, 핵심 정책을 ArchUnit fitness function 으로 **정적 강제**한다. DTO·domain·persistence 모델이 서로 새어 나가는 것을 막는 것이 핵심. - **이유**: 경계가 흐려지면 도메인/엔티티가 응답에 silent 직렬화되거나, request DTO 가 service layer 까지 leak 되거나, PATCH 가 기존 값을 silent overwrite 하는 회귀가 코드 리뷰만으로는 반복적으로 새어 나간다. 컨벤션을 *코드*(ArchUnit + wire-level 테스트)로 묶어 다음 작업자가 무심코 깨면 build 가 빨갛게 떨어지도록 했다. - **진행 단계**: **Phase C2 (코드) 구현 + 로컬 검증 완료.** `feature-boundary-validation-mapping-contract` 브랜치에서 ArchUnit rule, exception handler, envelope advice, mapper, sample 도메인(WorkLog) 까지 작성되어 코드 베이스에 존재한다. 운영 배포 / 실 DB 통합 테스트 / 측정값은 없다. ## Ground-truth 대조 (2026-06-04, ca-tmpl @fccb033 "경계 검증 계약 추가 및 sample 모듈 교체") `/home/donghyeon/workspace/ca-tmpl` 의 commit `fccb033` 코드를 직접 읽고 테스트를 재실행해 검증한 사실: - 패키지 root 는 `dev.caskeleton.*`. 브랜치 노트의 이전 `com.example.blog` 는 stale. - fccb033 시점에 **sample 모듈은 이미 `sample-portfolio`(WorkLog 도메인)** 로 교체된 상태다. 즉 "sample 모듈 교체"(sample-ticket → sample-portfolio)는 본 커밋에 포함되어 있다. 브랜치 노트가 참조한 `BoundaryDemoControllerWireTest` 는 교체 과정에서 **`WorkLogControllerWireTest` 로 re-home** 되었고, B1/B2/B3/B8 검증은 WorkLog 엔드포인트로 이전되었다. - 브랜치 노트는 일부 ArchUnit rule(controller 반환 타입, `@Valid` cascade depth)을 `planned` 으로 표기했으나, **fccb033 에서는 8개 boundary ArchUnit rule 이 모두 실제 구현되어 있다** (아래 §실제 구현 내용). 본 문서는 ground truth 를 우선해 이들을 `actually-implemented` 로 기록한다. - `./gradlew test verifyCleanArchitectureDependencies` (fccb033 worktree) → **BUILD SUCCESSFUL, 126 tests / 0 failures** (2026-06-04 재실행, exit 0). - 현재 repo HEAD 는 `db61075`(sibling `feature-business-rule-validation-contract`)로 더 진행되어 `Category`/`ResponseMeta` 등이 추가됨. 본 문서는 **fccb033 기준 사실만** 기록한다. ## 실제 구현 내용 (`actually-implemented`) ca-tmpl @fccb033 코드에서 직접 확인한 산출물: **shared-contract (stdlib-only, `dev.caskeleton.shared.*`)** - `request/Patch.java` — PATCH 필드의 3-state 값 객체. `absent()` / `ofNull()` / `of(value)` + `isAbsent()` / `isExplicitNull()` / `hasValue()`. 웹 어댑터가 Jackson-aware `JsonNullable` 를 이 Jackson-free 타입으로 변환해 application-core 가 wire 표현을 보지 않도록 함 (B2). - `error/MappingException.java` — 모든 경계 mapper(request→command, response shaping, outbound ACL)가 "구조는 멀쩡하나 의미상 매핑 불가" 일 때 던지는 sentinel `RuntimeException`. shared.error 에 두어 어느 모듈이든 cross-adapter 의존 없이 던질 수 있게 함 (B3 + B7). *(주의: 브랜치 노트 errors 로그는 `application.exception` 으로 이전했다고 기록하나, fccb033 ground truth 에서는 `shared.error` 에 위치 — 이후 모듈 승격/재배치의 결과.)* - `error/OperationalError.java` (enum) + `error/ApiErrorCode.java` (인터페이스, `code()`/`httpStatus()` int/`retryable()`) — `VALIDATION_FAILED(400,false)`, `MAPPING_FAILED(400,false)`, `BATCH_PARTIAL_FAILURE(200,false)`, `BAD_PARAMETER(400)`, `INTERNAL_ERROR(500,true)` 등. 전송 중립을 위해 Spring `HttpStatus` 대신 plain int. - `response/Envelope.java` / `response/BulkEnvelope.java` / `response/ApiError.java` — skeleton-wide 응답 봉투 타입. **adapter-web (`dev.caskeleton.adapter.web.*`)** - `error/GlobalExceptionHandler.java` (`@RestControllerAdvice extends ResponseEntityExceptionHandler`) — `ProblemDetail` import 0 (D5: RFC 7807 거부). `MappingException`→`MAPPING_FAILED`, `ConstraintViolationException`→`VALIDATION_FAILED`(field/message 리스트), `handleMethodArgumentNotValid` override→`VALIDATION_FAILED`(field/rejectedValue/message), `handleHttpMessageNotReadable` override→`VALIDATION_FAILED`(`{cause: }`), method-not-allowed/media-type/route-not-found override, catch-all→`INTERNAL_ERROR`. 모두 `ErrorResponseFactory` 단일 지점으로 envelope 빌드. - `envelope/EnvelopeBodyAdvice.java` (`ResponseBodyAdvice`) — 모든 JSON 컨트롤러 응답을 `Envelope.ok(body, traceId)` 로 자동 wrap. 이미 `Envelope`/`BulkEnvelope` 면 pass-through, null/void(DELETE 204)·비-JSON skip (D5/D6). **sample-portfolio (WorkLog 도메인 — 계약 실증)** - `adapter/web/dto/request/CreateWorkLogRequest.java` — `@GroupSequence({Syntax.class, Invariant.class, CreateWorkLogRequest.class})` + `@NotBlank/@Size/@NotNull(groups=Syntax.class)` + `@AssertTrue(groups=Invariant.class)` periodEnd≥periodStart. syntax→invariant short-circuit 실증 (B4). - `adapter/web/dto/request/UpdateWorkLogRequest.java` — 필드를 `JsonNullable` 로 받아 `Patch` 로 변환(`titlePatch()` 등). PATCH 3-state (B2). - `adapter/web/dto/request/SamplePolymorphicRequest.java` — `sealed interface` + record subtypes(`Text`/`Image`) + `@JsonTypeInfo(use=NAME, property="kind")` + `@JsonSubTypes` allowlist. allowlist 외 discriminator → `InvalidTypeIdException` (B5). - `adapter/web/mapper/WorkLogWebMapper.java` — 수기 mapper. 잘못된 link URI 면 `MappingException` wrap (B3). domain→response DTO 변환. - `adapter/outbound/repostats/RepoStatsAclMapper.java` (+ package-private `RawRepoStatsResponse`) — B7 ACL: normalization(lower-case)/masking(echoedToken drop)/public-field selection 후 domain 타입만 반환. raw 누락 시 `MappingException`. - `adapter/persistence/mapper/WorkLogPersistenceMapper.java` — 영속 매퍼. **app-bootstrap — ArchUnit fitness functions** (`architecture/CleanArchitectureTest.java`) — boundary rule **8개 모두 실 ArchRule (allowEmptyShould)**: - `request_dtos_do_not_silence_unknown_fields` — `..adapter.web..dto..` 의 class-level `@JsonIgnoreProperties(ignoreUnknown=true)` 금지 (B1). - `no_jackson_laissez_faire_subtype_validator` + `no_jackson_enable_default_typing_call` — CVE-2019-14379 RCE 벡터 차단 (B5). - `no_inheritable_thread_local` — virtual thread 누설 방지 (B6). - `controllers_do_not_return_domain_or_entity_types` — controller public 메서드가 `..domain.entity..`/`..persistence.entity..`/`..repository..` 반환 금지 (§Forbidden). *브랜치 노트는 `planned` 였으나 fccb033 에 구현됨.* - `application_methods_do_not_accept_web_dtos` — application public 메서드가 `..adapter.web..dto..` 파라미터 수용 금지 (§Forbidden). *동일하게 fccb033 에 구현됨.* - `no_problem_detail_usage` — `org.springframework.http.ProblemDetail` import 차단 (D5). - `no_merge_patch_json_media_type_string` — custom ArchCondition 으로 `application/merge-patch+json` 어노테이션 참조 차단 (B2). - `valid_cascade_depth_at_most_three` — custom ArchCondition 으로 `@Valid` cascade depth ≤ 3 (B4 DoS 방어). *브랜치 노트는 `planned` 였으나 fccb033 에 구현됨.* - `outbound_adapter_method_returns_only_domain_or_primitives` — outbound public 메서드가 raw external 응답 타입 escape 금지 (B7 ACL). - 각 rule 은 `ArchitectureViolationFixtureTest` 의 의도된 위반 fixture(`JsonIgnoreUnknownRequestFixture`, `DefaultTypingFixture`, `InheritableThreadLocalFixture`, `DomainReturningControllerFixture`, `WebDtoAcceptingApplicationFixture`, `ProblemDetailUsingFixture`, `MergePatchJsonFixture`, `DeepCascadeRequestFixture`)로 catch 동작을 보증 (violations-as-data). ## 로컬/dev 검증 (`locally-verified`) - **wire-level 테스트** `WorkLogControllerWireTest` (`@WebMvcTest`/`@TestPropertySource`) 9 케이스: envelope wrap, 404, blank-title validation, unknown-field 거부(B1), unmappable-link→`MAPPING_FAILED`(B3), PATCH present-only 교체 + explicit-null 수용(B2), repo-stats domain via envelope, bulk partial → `BATCH_PARTIAL_FAILURE`(B8). - **unit/contract 테스트**: `SamplePolymorphicRequestTest`(B5 sealed type 4 케이스), `BasicPolymorphicTypeValidatorAllowlistTest`(B5 allowlist 4 케이스), `BulkEnvelopeTest`(3 케이스), `GlobalExceptionHandlerTest`, `EnvelopeBodyAdviceTest`, `RepoStatsAclMapperTest`(B7), `WorkLogPersistenceMapperTest`, `DomainExceptionHandlerTest`, `OperationalErrorTest`. - **virtual-thread MDC**: `VirtualThreadMdcPropagationTest`(unit) + `VirtualThreadMdcE2ETest`(`@SpringBootTest(RANDOM_PORT)` 실 Tomcat + 실 가상스레드 + 실 `RequestLoggingFilter` + `TestRestTemplate`) — server-generated `requestId` 와 client `X-Request-Id` 두 경로가 컨트롤러까지 도달함을 wire-level pin (B6). - **ArchUnit + 위반 fixture**: `CleanArchitectureTest` + `ArchitectureViolationFixtureTest` 전체 green. - **전체 빌드**: `./gradlew test verifyCleanArchitectureDependencies` (fccb033) → **126 tests / 0 failures**, 2026-06-04 재실행 exit 0. - 검증 범위는 JVM 단위/슬라이스/슬라이스-wire/e2e(in-process Tomcat) + 정적 분석까지. **실 DB(Testcontainers) 통합 테스트는 없음** (persistence 매퍼는 unit 레벨). ## 운영 검증 (`prod-verified`) **없음.** 운영 환경에 배포된 적이 없다. 트래픽·측정값·인시던트·릴리즈 노트 어느 것도 없다. 본 패스는 enforcement + reference + unit/contract + wire-level + e2e(in-process) 단계까지다. ## 문서/계획만 존재 (`documented-only` / `planned`) 다음은 설계/문서/위임 상태이며 **면접에서 "구현했다 / 검증했다"고 말하면 안 된다**. - **B7-2 실 `WebClient`/`RestClient` + WireMock 통합**: `planned`. 현 패스의 outbound 는 HTTP fetch 를 추상화한 형태이고 실 외부 HTTP 왕복은 미검증. - **B8-2 OpenAPI response shape 분기(`oneOf`) 명시**: `planned`. OpenAPI 스펙 자체가 부재해 구현 보류. - **B2 RFC 7396 미채택 사실의 OpenAPI 문서화**: `planned` (OpenAPI 부재). - **request DTO primitive→wrapper 강제 ArchUnit rule** (B1 component-type): `planned`. Jackson 4-종 스위치 자체는 설정/테스트로 확인되나 component-type ArchUnit rule 은 미작성. - **실 DB 통합(@DataJpaTest / Testcontainers)**, `@Version` 낙관적 락: `planned` (후속 브랜치). - **MapStruct generated mapper exemption rule**: `documented-only` / `needs-confirmation`. 현 구현은 수기 mapper 만 사용하며 MapStruct 는 optional 계약으로만 존재. ## 면접에서 말할 수 있는 범위 ### 자신 있게 답할 수 있는 질문 - 입력 경계에서 검증/매핑 책임을 어떻게 분리했는가 — Bean Validation `@GroupSequence` 로 syntax→invariant short-circuit, request DTO→command 수기 mapper, response 는 DTO 만 노출. - `MethodArgumentNotValidException` / `HttpMessageNotReadableException` 을 왜 `VALIDATION_FAILED` 로, mapper 내부 실패를 왜 `MappingException`→`MAPPING_FAILED` 별도 카테고리로 분류했는가 (Spring 이 전자는 자동 처리, 후자는 안 하므로). - PATCH 의 absent/explicit-null/value 3-state 를 `JsonNullable`→`Patch` 로 어떻게 구분했고, 구분 안 하면 어떤 silent overwrite 버그가 나는가. - CVE-2019-14379 (Jackson default typing gadget chain RCE) 를 ArchUnit 으로 `enableDefaultTyping()` 호출과 `LaissezFaireSubTypeValidator` 참조를 정적 차단하고, 안전한 `@JsonTypeInfo`+`@JsonSubTypes` / `BasicPolymorphicTypeValidator` allowlist 만 허용한 방법. - RFC 7807 ProblemDetail 을 왜 거부하고 custom envelope 를 썼는가, 그 결정을 `no_problem_detail_usage` ArchUnit 으로 회귀 차단한 방법. - B7 outbound ACL — 외부 응답 raw 타입이 domain 으로 leak 되지 않도록 mapper + ArchUnit(`outbound_adapter_method_returns_only_domain_or_primitives`)으로 강제한 방법. - violations-as-data — 각 ArchUnit rule 이 의도된 위반 fixture 를 실제로 잡는지 네거티브 테스트로 보증한 패턴. ### 적당히 답할 수 있는 질문 - virtual thread(`spring.threads.virtual.enabled`) 환경에서 `InheritableThreadLocal` 이 왜 위험하고 MDC/`RequestContextHolder` 로 어떻게 context 를 전파하는가 (단 실 프로덕션 트래픽 검증은 안 함). - MapStruct vs 수기 mapper 의 trade-off (현 구현은 수기 mapper 채택, MapStruct 는 미사용). ### 답하면 안 되는 질문 (모른다고 해야 함) - "운영에서 이 검증/매핑 계약이 인시던트를 막은 사례가 있는가? 성능을 측정했는가?" → **운영 배포 없음, 측정 없음.** - "outbound ACL 을 실 외부 API + WireMock 으로 통합 검증했는가?" → **안 함. HTTP fetch 추상화 단계.** - "PATCH/검증을 실 DB 통합 테스트로 끝까지 돌렸는가?" → **persistence 는 unit 레벨. Testcontainers 통합 없음.** - "OpenAPI 로 bulk/단일 응답 shape 분기를 명시했는가?" → **OpenAPI 스펙 부재. `planned`.** ## 과장 금지 지점 - **"운영에서 검증했다 / prod 에서 돌고 있다" → 금지.** 로컬 단위/슬라이스/e2e(in-process Tomcat) + 정적 분석까지가 검증 범위. - **"실 DB 통합 테스트로 PATCH/매핑을 검증했다" → 금지.** persistence 매퍼는 unit 레벨, Testcontainers 없음. - **"4-layer validation(syntax/policy/invariant/persistence integrity)은 표준 분류다" → 금지.** Bean Validation spec 은 이 taxonomy 를 정의하지 않는다. ca-tmpl 내부 설계 결정이다([[wiki/concepts/boundary-validation-and-dto-mapping]] 참조). - **"controller 반환 타입/cascade depth ArchUnit 은 계획만 했다" → (옛 브랜치 노트 표현) 정정.** fccb033 ground truth 에서는 둘 다 구현되어 있다. - **"ArchUnit 으로 막았으니 RCE/leak 이 원천 불가능하다" → 단정 금지.** 정적 분석은 바이트코드에서 탐지 가능한 carrier(어노테이션/import/호출)만 잡는다. 메서드 본문 내 free-form 문자열 등은 한계가 있다(코드 주석에 명시됨). - **"`MappingException` 위치가 `application.exception` 이다" → fccb033 기준 정정.** ground truth 에서는 `shared.error` 에 있다. ### Blog-topic ingest: boundary-validation-mapper-responsibility-map (2026-07-02) [[raw/blog-topics/boundary-validation-mapper-responsibility-map-2026-07-02]] 는 입력 syntax, application policy, domain invariant, persistence integrity, mapper normalization 책임을 한 계층에 몰지 않는 경계 설계를 블로그로 풀기 위한 raw seed다. - **locally-verified 로 말할 수 있는 부분**: `Patch` 3-state, `MappingException`, DTO/mapper boundary ArchUnit rule, validation/mapping wire·unit test 범위. - **project-local policy 로 말할 부분**: syntax/policy/invariant/persistence integrity/normalization 책임 분리는 ca-tmpl 내부 taxonomy다. - **블로그 전 과장 방지**: 모든 validation 책임을 해결하는 보편 구조처럼 쓰지 않고, fccb033 기준 구현·검증 범위와 미구현 OpenAPI/DB integration 범위를 분리한다. ### Blog-topic ingest: archunit-jackson-default-typing-cve block (2026-07-02) [[raw/blog-topics/archunit-jackson-default-typing-cve-2019-14379-block-2026-05-29]] 는 Jackson default typing RCE 진입점(`enableDefaultTyping`, `LaissezFaireSubTypeValidator`)을 ArchUnit fitness function으로 차단한 글감이다. - **locally-verified 로 말할 수 있는 부분**: `no_jackson_laissez_faire_subtype_validator`, `no_jackson_enable_default_typing_call`, `DefaultTypingFixture`가 boundary canonical에 이미 구현/검증 범위로 기록돼 있다. - **블로그 전 과장 방지**: CVE 전체를 제거했다고 쓰지 않고, ca-tmpl 코드에서 특정 위험 API 호출/참조를 정적 rule로 차단한 범위로 제한한다. ## 관련 개념 - [[wiki/concepts/boundary-validation-and-dto-mapping]] - [[wiki/concepts/transaction-boundary-abstraction]] ## Sources - [[raw/branch-notes/feature-boundary-validation-mapping-contract]] — 결정(D1~D15)·Decision Evidence Map·Claims To Verify·구현 결과(5/6차 패스)·wiki 추출 대상 - [[raw/blog-topics/boundary-validation-mapper-responsibility-map-2026-07-02]] — boundary validation/mapper 책임 분리 블로그 글감 raw seed. canonical 반영 범위: verified boundary/mapping 구현 + project-local 책임 taxonomy + 과장 금지 항목. - [[raw/blog-topics/archunit-jackson-default-typing-cve-2019-14379-block-2026-05-29]] — Jackson default typing CVE static block 블로그 글감 raw seed. - [[raw/project-notes/ca-skeleton-operational-contract]] — §6 Operational Error Category(`VALIDATION_FAILED`/`MAPPING_FAILED`/`BATCH_PARTIAL_FAILURE` 등록), §20 Skeleton Blueprint package convention - [[raw/official-docs/validation-jakarta-bean-validation-3.0-spec]] — `@GroupSequence` short-circuit, `@Valid` cascade - [[raw/official-docs/spring-mvc-rest-exception-handling]] — `HttpMessageNotReadableException`/`MethodArgumentNotValidException` → VALIDATION 분류 - [[raw/official-docs/patch-json-merge-rfc7396]] — PATCH null=deletion (미채택 근거) - [[raw/official-docs/schema-jackson-polymorphic-deserialization]] — CVE-2019-14379 + allowlist API (B5) - ca-tmpl @fccb033 코드 (ground-truth): `src/shared-contract/.../request/Patch.java` · `.../error/{MappingException,OperationalError,ApiErrorCode}.java`, `src/adapter-web/.../error/GlobalExceptionHandler.java` · `.../envelope/EnvelopeBodyAdvice.java`, `src/sample-portfolio/.../adapter/web/{dto/request,mapper}/*.java` · `.../adapter/outbound/repostats/RepoStatsAclMapper.java`, `src/app-bootstrap/.../architecture/{CleanArchitectureTest,ArchitectureViolationFixtureTest}.java` · `.../controller/WorkLogControllerWireTest.java` ## Cluster / 묶음 - [[wiki/blog/ca-tmpl-boundary-validation-mapping-2026-07-02]]