10 KiB
10 KiB
title, source_type, status, confidence, tags, related_projects, last_reviewed
| title | source_type | status | confidence | tags | related_projects | last_reviewed | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 경계 검증과 DTO↔도메인 매핑 (Bean Validation · MapStruct vs 수기 mapper · Patch partial-update) | llm-generated | draft | medium |
|
|
2026-06-04 |
경계 검증과 DTO↔도메인 매핑
Layer:
wiki/concepts/— 일반 개념. 내 프로젝트 사실은 wiki/projects/ca-tmpl/boundary-validation-mapping 참조.
Summary
웹 애플리케이션의 입력 경계(request boundary)에서는 두 가지 책임이 동시에 생긴다: (1) 들어온 데이터가 형식적으로 올바른지 검증하고, (2) 외부 표현(DTO)을 내부 모델(domain / command)로 변환(mapping) 하는 것이다.
- Bean Validation (Jakarta Validation, JSR 380 / 3.0):
@NotNull,@Size,@Valid같은 선언적 제약을 DTO 필드/메서드에 붙여 프레임워크가 자동 검증하게 하는 표준. Spring MVC 는 컨트롤러 파라미터에@Valid/@Validated가 붙으면 본문 바인딩 직후 검증을 수행하고, 실패 시MethodArgumentNotValidException을 던진다. - validation-at-boundary 원칙: 검증은 가능한 한 입력 경계 한 곳 에서 fail-fast 로 끝내고, 안쪽 레이어(application/domain)는 이미 검증된 값만 받는다는 설계. 단, 형식(syntax) 검증과 도메인 불변식(invariant) 검증은 책임이 다르므로 같은 어노테이션 한 줄로 뭉뚱그리지 않는다.
- DTO↔domain mapping: 외부에 노출되는 DTO 와 내부 도메인 객체를 분리하고 그 사이를 변환하는 코드. 변환 도구는 수기(manual) mapper 와 MapStruct 같은 코드 생성기(generator) 두 갈래가 있다.
- partial-update (PATCH) semantics: PATCH 요청에서 "필드 없음(absent) / 명시적 null / 값 있음" 세 상태를 구분해야 silent overwrite 를 막을 수 있다.
Standard (공식 정의)
- Jakarta Bean Validation 3.0 (official-standard): class-level constraint 는 "한 클래스의 여러 property 를 동시에 보는 상태 검증"을 위한 것이고(JBV-3.0-C1),
ConstraintValidator는 클래스 인스턴스를 받아 여러 필드에 동시 접근할 수 있다(JBV-3.0-C2).@GroupSequence를 쓰면 group 을 순서대로 실행하다 한 group 이 실패하면 다음 group 을 건너뛴다(short-circuit) — syntax 검증을 먼저 통과해야 invariant 검증이 돈다는 패턴의 normative 근거(JBV-3.0-C3).@Valid는 중첩 객체로 검증을 cascade(전파) 시킨다(JBV-3.0-C4). 단, Bean Validation 자체는 syntax/invariant 라는 레이어 이름을 정의하지 않는다 — 그 분류는 애플리케이션 설계 결정이다. - Spring MVC REST exception handling (official-vendor-doc):
HttpMessageNotReadableException(JSON 파싱 실패) 과MethodArgumentNotValidException(Bean Validation 실패) 은 모두 Spring 내장ErrorResponse구현체이고ResponseEntityExceptionHandler가 normative 하게 처리한다(SPRING-MVC-EXC-C1/C2/C4/C5). 즉 이 두 예외는 표준적으로 검증 실패(400) 카테고리로 분류된다. - RFC 7396 (JSON Merge Patch) (official-standard): merge patch 에서
null값은 "해당 필드 삭제"를 의미한다(RFC7396-C2). 따라서 "명시적 null" 을 다른 의미로 쓰려는 API 는 RFC 7396 merge patch 를 그대로 채택하면 충돌한다(RFC7396-C3). 배열 부분 수정도 불가하다(RFC7396-C4). - MapStruct (도구): 컴파일 타임에 mapper 구현 코드를 생성하는 어노테이션 프로세서. 리플렉션 없이 동작하지만, 생성된 코드가 architecture 규칙(예: 도메인 직접 접근 금지)을 우회할 수 있어 별도 exemption 관리가 필요하다. (※ MapStruct 도구 선택 자체는 공식 표준이 권고하는 사항이 아니라 프로젝트 trade-off 결정이다.)
한계 / 주의점
- 4-layer validation 분류(syntax / policy / invariant / persistence integrity)는 표준이 아니다. Bean Validation spec 은 이런 taxonomy 를 정의하지 않는다. 레이어를 나누는 것은 설계 결정이며, 잘못 나누면 같은 검증이 두 곳에서 중복되거나 빠진다.
- MapStruct vs 수기 mapper 는 정답이 없는 trade-off. 수기 mapper 는 boilerplate 가 많지만 동작이 투명하다. MapStruct 는 코드량을 줄이지만 generated code 가 architecture 경계를 silent 하게 leak 할 수 있고, 매핑 누락이 컴파일 시점에 드러나지 않을 수 있다.
- PATCH 의 null/absent 혼동은 흔한 버그다. Java record 의 기본 매핑으로 PATCH 를 구현하면 요청에 없던 필드가
null로 들어와 기존 값을 덮어쓰는 silent overwrite 가 발생한다.Optional<T>또는JsonNullable<T>(openapi-generator) 같은 3-state wrapper 가 필요하다. - 검증을 경계에서만 한다고 도메인 불변식이 보장되지는 않는다. 형식 검증(DTO)과 도메인 불변식(application/domain)은 별개다. DTO 검증만으로 "도메인이 안전하다"고 말하면 안 된다.
@Validcascade 의 무한/깊은 재귀는 DoS 표면이 될 수 있다. 중첩 깊이에 상한을 두는 것은 spec 이 아니라 운영적 방어 결정이다.
Project Application
- ca-tmpl 은 입력 경계의 검증/매핑 책임을 명시적으로 고정하고, 일부 정책을 ArchUnit fitness function 으로 정적 강제했다. 구체적 구현 사실·검증 등급은 wiki/projects/ca-tmpl/boundary-validation-mapping 참조.
- 관련 트랜잭션 경계 추상화는 wiki/concepts/transaction-boundary-abstraction / wiki/projects/ca-tmpl/transaction-boundary-abstraction.
Claim-backed Knowledge
아래는 본 개념을 뒷받침하는 raw official-doc claim 인용. company-tech-blog 는 사례일 뿐 공식 best practice 로 격상하지 않는다.
| Knowledge Point | Supporting Claims | Confidence | Notes |
|---|---|---|---|
| class-level constraint 는 한 클래스의 여러 property 상태를 함께 검증한다 | raw/official-docs/validation-jakarta-bean-validation-3.0-spec JBV-3.0-C1 | high | constraint 의 목적 근거. syntax/invariant 레이어 이름은 spec 미규정 |
@GroupSequence 는 group 을 순차 실행하다 실패 시 후속 group 을 short-circuit 한다 |
raw/official-docs/validation-jakarta-bean-validation-3.0-spec JBV-3.0-C3 | high | syntax→invariant 단계 분리 패턴의 normative 근거 |
@Valid 는 중첩 객체로 검증을 cascade 한다 |
raw/official-docs/validation-jakarta-bean-validation-3.0-spec JBV-3.0-C4 | high | cascade 메커니즘 근거. depth 상한은 설계 결정 (spec 미규정) |
HttpMessageNotReadableException 은 Spring 이 normative 하게 처리하는 내장 예외 |
raw/official-docs/spring-mvc-rest-exception-handling SPRING-MVC-EXC-C4 | high | JSON 파싱 실패 → 검증(400) 분류 근거 |
MethodArgumentNotValidException 은 field error 를 담아 normative 처리된다 |
raw/official-docs/spring-mvc-rest-exception-handling SPRING-MVC-EXC-C5 | high | Bean Validation 실패 → 검증(400) + field error shape 근거 |
JSON Merge Patch 의 null 은 필드 삭제를 의미한다 |
raw/official-docs/patch-json-merge-rfc7396 RFC7396-C2 | high | PATCH 에서 null/absent 구분이 필요한 이유. ca-tmpl 은 merge patch 미채택 |
내가 설명할 수 있어야 하는 것
- Bean Validation 의
@Valid/@Validated/@GroupSequence가 각각 무엇이고, syntax 검증과 도메인 invariant 검증을 왜 분리하는가. MethodArgumentNotValidException과HttpMessageNotReadableException이 왜 둘 다 "검증 실패(400)" 로 분류되는가, mapper 내부 예외는 왜 별도 카테고리가 필요한가.- DTO↔domain mapping 에서 MapStruct 와 수기 mapper 의 trade-off (boilerplate vs architecture leak / 컴파일 안전성).
- PATCH 의 absent / explicit-null / value 3-state 를 구분하지 않으면 어떤 버그(silent overwrite)가 생기는가,
Optional/JsonNullable로 어떻게 구분하는가. - RFC 7396 merge patch 의 null=deletion semantics 와, 이를 채택하지 않는 API 가 왜
application/merge-patch+jsoncontent type 을 쓰면 안 되는가.
Interview Questions
- "request 검증을 어디서 하나요? 컨트롤러? 서비스? 도메인?" → 형식 검증은 경계(DTO), 도메인 불변식은 application/domain. 한 줄 어노테이션으로 다 끝낸다는 답은 위험.
- "
@Valid와@Validated차이는?" →@Validated는 Spring 의 group 지원 + 메서드 레벨 검증,@Valid는 표준 cascade. - "PATCH 에서 어떤 필드만 바꾸고 싶을 때 null 을 어떻게 처리하나요?" → absent vs explicit-null 구분, 3-state wrapper.
- "DTO 와 도메인 객체를 왜 분리하나요? MapStruct 와 수기 매핑 중 무엇을 쓰나요?" → 노출 경계 분리 + 도구 trade-off.
Do Not Overclaim
- "Bean Validation 이 syntax/invariant 를 알아서 나눠준다" → 금지. spec 은 레이어를 정의하지 않는다.
@GroupSequence로 순서 는 줄 수 있지만 분류는 설계자가 한다. - "MapStruct 가 수기 mapper 보다 우월하다" → 금지. generated code 의 architecture leak / 매핑 누락 trade-off 가 있다.
- "DTO 검증을 했으니 도메인이 안전하다" → 금지. 형식 검증과 도메인 불변식은 별개.
- "PATCH 의 null 은 항상 삭제다(RFC 7396)" → 단정 금지. RFC 7396 의 정의일 뿐, 이를 채택하지 않는 API 도 많다.
Sources
- raw/official-docs/validation-jakarta-bean-validation-3.0-spec — Jakarta Bean Validation 3.0 normative (class-level constraint, group sequence,
@Validcascade) - raw/official-docs/spring-mvc-rest-exception-handling — Spring MVC
ResponseEntityExceptionHandler처리 예외 목록 (HttpMessageNotReadableException/MethodArgumentNotValidException) - raw/official-docs/patch-json-merge-rfc7396 — RFC 7396 JSON Merge Patch (null=deletion semantics)
- raw/official-docs/schema-jackson-polymorphic-deserialization — Jackson polymorphic deserialization 보안 지침 (allowlist, CVE-2019-14379) — 경계 역직렬화 보안 맥락
- raw/branch-notes/feature-boundary-validation-mapping-contract — 본 개념을 도출한 ca-tmpl 경계 검증/매핑 계약 branch
- wiki/projects/ca-tmpl/boundary-validation-mapping — 내 프로젝트 적용 사실