5.6 KiB
DTO / Domain / Entity separation 기준
목적
DTO, Domain, Entity는 이름만 다른 비슷한 객체가 아니라 서로 다른 경계와 책임을 가진 모델이다.
형태가 비슷하더라도 역할이 다르면 분리한다.
공식 의미
- DTO는 프로세스/경계 사이에서 데이터를 운반하는 객체다.
- Entity는 persistence provider가 관리하는 영속 모델이다.
- Spring request binding은 강력하지만, 외부 입력을 객체 그래프에 직접 바인딩하는 것은 보안상 주의가 필요하다.
- 따라서 web 입력 모델, 비즈니스 모델, persistence 모델을 하나로 섞지 않는 것이 안전하다.
기본 규칙
1. DTO는 경계 데이터 모델이다
DTO는 다음 경계에서만 사용한다.
- HTTP request / response
- external API request / response
- message payload
- batch/file I/O payload
DTO의 책임:
- 데이터 운반
- 직렬화/역직렬화 경계 표현
- validation annotation 수용 가능
- API 계약 표현
DTO의 책임이 아닌 것:
- 비즈니스 규칙
- 도메인 불변식 유지
- 영속성 상태 관리
- repository/service 호출
2. Domain은 비즈니스 의미 모델이다
Domain은 다음을 표현한다.
- entity
- value object
- domain service
- domain policy
- 불변식
- 상태 전이 의미
Domain의 책임:
- 비즈니스 의미
- 유효한 상태 보장
- 행위와 규칙 표현
Domain의 책임이 아닌 것:
- JSON 구조
- HTTP 요청/응답 형식
- JPA annotation/persistence lifecycle
- 외부 API payload 구조
3. Entity는 persistence 모델이다
Entity는 DB와 persistence provider가 다루는 모델이다.
Entity의 책임:
- 테이블/컬럼/관계 매핑
- persistence lifecycle 대응
- 저장 가능한 상태 표현
Entity의 책임이 아닌 것:
- API 응답 계약
- 비즈니스 정책 결정
- 외부 연동 payload 표현
4. 하나의 타입으로 세 역할을 겸하지 않는다
다음은 기본 금지한다.
- request DTO를 그대로 domain으로 사용
- JPA entity를 그대로 response DTO로 반환
- domain object에 JPA/JSON/validation annotation을 한꺼번에 섞기
형태가 비슷해도 책임이 다르면 타입을 분리한다.
5. 경계마다 변환을 명시한다
기본 흐름:
- request DTO -> command / domain input
- entity -> domain
- domain -> response DTO
- external payload -> internal model
변환은 mapper/assembler/factory 같은 명시적 경계에서 수행한다.
6. DTO에는 validation을 둘 수 있지만, 최종 의미 검증은 domain이 맡는다
DTO에 둘 수 있는 것:
@NotBlank@Size- 형식 검증
- 웹 입력 범위 검증
하지만 다음은 domain 책임이다.
- 불변식
- 상태 전이 타당성
- 정책 기반 허용 여부
- aggregate 일관성
즉 DTO validation이 domain validation을 대체하지 않는다.
7. Entity의 nullable/관계/지연로딩 특성을 domain에 그대로 전파하지 않는다
DB 제약 때문에 entity가 nullable/관계 중심일 수는 있다.
하지만 domain은 비즈니스 invariant 기준으로 더 엄격할 수 있다.
기본:
- entity -> domain 변환 시 검증/정규화
- domain은 persistence 편의보다 비즈니스 의미 우선
8. Response DTO는 domain을 그대로 노출하지 않는다
response는 외부 계약이다.
기본적으로 domain object를 그대로 JSON으로 노출하지 않는다.
이유:
- 내부 구조 변경이 외부 계약에 새어 나간다
- 민감정보/불필요 필드 노출 위험
- 직렬화 shape가 domain 설계를 오염시킨다
9. Request DTO를 entity에 직접 바인딩하지 않는다
Spring DataBinder/WebDataBinder는 강력하지만 보안상 주의가 필요하다.
따라서 외부 입력을 entity나 깊은 도메인 객체에 직접 바인딩하지 않는다.
기본:
- request DTO에만 바인딩
- 이후 명시적 변환을 거쳐 command/domain으로 이동
10. Entity를 domain과 1:1로 맞추려 하지 않는다
entity와 domain은 비슷할 수 있지만 항상 같아야 할 필요는 없다.
예:
- entity는 FK/nullable/지연 로딩 중심
- domain은 value object/invariant/행위 중심
“필드가 같아 보이니 하나로 합친다”를 금지한다.
11. DTO는 record/단순 데이터 구조를 우선 검토
DTO는 경계 데이터 운반이 목적이므로, 불변/단순 구조를 우선 검토한다. 단, framework binding/serialization 요구사항이 있으면 그 제약을 따른다.
12. Domain은 DTO naming을 따라가지 않는다
도메인 타입 이름은 API 필드명/JSON 필드명보다 비즈니스 의미를 우선한다.
금지 예:
UserResponseNameProviderRequestCode
도메인은 business language를 사용한다.
13. Entity는 persistence 편의 메서드를 가질 수 있지만 도메인 규칙 중심 타입이 되지 않게 한다
entity에 persistence 편의 메서드가 있을 수는 있다.
하지만 핵심 비즈니스 규칙을 entity/JPA lifecycle에 과도하게 묶지 않는다.
14. Mapping 비용보다 경계 명확성이 더 중요하다
DTO/Domain/Entity 분리는 변환 코드가 늘 수 있다.
그러나 그 비용보다:
- 경계 명확성
- 보안성
- 변경 범위 제한
- 직렬화/영속성 오염 방지 의 이익이 더 크다.
프로젝트 기준 요약
- DTO = 경계 데이터
- Domain = 비즈니스 의미
- Entity = persistence 모델
- 하나의 타입으로 세 역할 겸용 금지
- request는 DTO에만 바인딩
- DTO validation과 domain invariant를 분리
- entity nullable/관계를 domain에 그대로 전파하지 않음
- response DTO로 외부 계약을 명시