Files
llm-wiki/wiki/concepts/boundary-validation-and-dto-mapping.md
T

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
backend
validation
mapper
dto
bean-validation
boundary
ca-skeleton
ca-tmpl
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) mapperMapStruct 같은 코드 생성기(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 검증만으로 "도메인이 안전하다"고 말하면 안 된다.
  • @Valid cascade 의 무한/깊은 재귀는 DoS 표면이 될 수 있다. 중첩 깊이에 상한을 두는 것은 spec 이 아니라 운영적 방어 결정이다.

Project Application

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 검증을 왜 분리하는가.
  • MethodArgumentNotValidExceptionHttpMessageNotReadableException 이 왜 둘 다 "검증 실패(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+json content 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