Files
llm-wiki/vault/30-knowledge/projects/ca-tmpl/boundary-validation-mapping.md
T

19 KiB

title, source_type, status, confidence, tags, related_projects, last_reviewed
title source_type status confidence tags related_projects last_reviewed
ca-tmpl - 입력 경계 검증 & DTO↔도메인 매핑 계약 (Bean Validation · Patch · ACL · 정적 강제) project verified high
ca-tmpl
validation
mapper
boundary
dto
archunit
actually-implemented
ca-tmpl
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<T> 를 이 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 거부). MappingExceptionMAPPING_FAILED, ConstraintViolationExceptionVALIDATION_FAILED(field/message 리스트), handleMethodArgumentNotValid override→VALIDATION_FAILED(field/rejectedValue/message), handleHttpMessageNotReadable override→VALIDATION_FAILED({cause: <Jackson exception simpleName>}), 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<T> 로 받아 Patch<T> 로 변환(titlePatch() 등). PATCH 3-state (B2).
  • adapter/web/dto/request/SamplePolymorphicRequest.javasealed 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_usageorg.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 내부 실패를 왜 MappingExceptionMAPPING_FAILED 별도 카테고리로 분류했는가 (Spring 이 전자는 자동 처리, 후자는 안 하므로).
  • PATCH 의 absent/explicit-null/value 3-state 를 JsonNullable<T>Patch<T> 로 어떻게 구분했고, 구분 안 하면 어떤 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<T> 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로 차단한 범위로 제한한다.

관련 개념

Sources

Cluster / 묶음