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 |
|
|
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 반환 타입,
@Validcascade 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(siblingfeature-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-awareJsonNullable<T>를 이 Jackson-free 타입으로 변환해 application-core 가 wire 표현을 보지 않도록 함 (B2).error/MappingException.java— 모든 경계 mapper(request→command, response shaping, outbound ACL)가 "구조는 멀쩡하나 의미상 매핑 불가" 일 때 던지는 sentinelRuntimeException. 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)등. 전송 중립을 위해 SpringHttpStatus대신 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) —ProblemDetailimport 0 (D5: RFC 7807 거부).MappingException→MAPPING_FAILED,ConstraintViolationException→VALIDATION_FAILED(field/message 리스트),handleMethodArgumentNotValidoverride→VALIDATION_FAILED(field/rejectedValue/message),handleHttpMessageNotReadableoverride→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.java—sealed interface+ record subtypes(Text/Image) +@JsonTypeInfo(use=NAME, property="kind")+@JsonSubTypesallowlist. allowlist 외 discriminator →InvalidTypeIdException(B5).adapter/web/mapper/WorkLogWebMapper.java— 수기 mapper. 잘못된 link URI 면MappingExceptionwrap (B3). domain→response DTO 변환.adapter/outbound/repostats/RepoStatsAclMapper.java(+ package-privateRawRepoStatsResponse) — 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.ProblemDetailimport 차단 (D5).no_merge_patch_json_media_type_string— custom ArchCondition 으로application/merge-patch+json어노테이션 참조 차단 (B2).valid_cascade_depth_at_most_three— custom ArchCondition 으로@Validcascade 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-generatedrequestId와 clientX-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<T>→Patch<T>로 어떻게 구분했고, 구분 안 하면 어떤 silent overwrite 버그가 나는가. - CVE-2019-14379 (Jackson default typing gadget chain RCE) 를 ArchUnit 으로
enableDefaultTyping()호출과LaissezFaireSubTypeValidator참조를 정적 차단하고, 안전한@JsonTypeInfo+@JsonSubTypes/BasicPolymorphicTypeValidatorallowlist 만 허용한 방법. - RFC 7807 ProblemDetail 을 왜 거부하고 custom envelope 를 썼는가, 그 결정을
no_problem_detail_usageArchUnit 으로 회귀 차단한 방법. - 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
- 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 —
@GroupSequenceshort-circuit,@Validcascade - 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