[TIL] DTO 나누는 기준, Entity에서 DTO 생성하는 방식, @Valid
in TIL on Til Last modified at:
2025-04-30 TIL
📝 TIL (Today I Learned)
🔗 원본 이슈: #49
📅 작성일: 2025-04-30
🔄 최종 수정: 2025년 05월 21일
🍀 새롭게 배운 것
1️⃣ DTO 나누는 기준
DTO를 여러 개 만들다 보니, 어떤 경우에 새로 생성을 해야하고, 어떤 경우에 쓰던 DTO를 사용해도 되는건지 정확하게 알고싶었다. 깃블로그 정리
- Response DTO에서도 도메인 모델은 같아도 API의 응답 목적에 따라 DTO를 다르게 한다는 것을 알게되었다.
- 예를 들어, |클래스|목적|어떤 API에 사용되는가| |——|—|—————| |
PeerReviewDetailResponse
|단일 리뷰의 상세 정보 제공|리뷰 생성 응답, 리뷰 상세 조회 등| |UserReviewSummaryResponse
|특정 사용자가 받은 리뷰들의 종합 통계 정보 제공|사용자 리뷰 평균 조회 API| |ProjectReviewStatusResponse
|프로젝트 내에서 리뷰가 완료되었는지 확인|프로젝트 리뷰 진행 상태 확인 API| - 즉, 하나의
PeerReviewResponse
로 모든 경우를 처리하면 과한 데이터가 응답되거나, 불필요한 필드를 채워야하는 문제가 생김 - 각 API의 의미, 응답 범위, 데이터의 성격이 다르기 때문에 분리를 해줘야 함
또한 너무 단순한 DTO를 별도로 여러 개 만드는 것이 오히려 복잡해지진않을까 고민했다.
- 단순해도 별도 DTO를 유지할 수 있는 이유
- 응답 목적이 분명히 다름
- 예를 들어,
PeerReviewDetailResponse
는 리뷰 하나하나의 전체 정보를 보여줌 UserReviewSummaryResponse
는 집계 결과만 보여줌- ➡️ DTO는 목적에 따라 나눠야지, 포함된 데이터 수가 적다고 합치면 안됨
- 예를 들어,
- 확장성 고려
- 오늘은 평균 점수와 코멘트만 보여주지만, 내일은 “피어리뷰 받은 날짜 리스트”, “리뷰 작성자 정보” 등이 추가될 수 있음
- API 유지보수 용이
- 응답 스펙이 명확하게 고정된 하나의 DTO로 분리되어 있으면, 클라이언트와의 연동 시에도 변경 위험을 줄일 수 있음
- 어떤 API는 summary만 주고, 어떤 API는 리뷰 detail 리스트를 따로 주는 등 조합이 자유로움
- 응답 크기 / 성능 최적화
- 하나의 응답에 모든 리뷰 데이터를 다 담으면 리스트가 커짐 -> 느려질 수 있음
- 필수정보만 담아 빠르고 가볍게 구현 가능
- 응답 목적이 분명히 다름
따라서 API 엔드포인트 별로, 응답 목적 별로 DTO를 나눠서 생성하는 것이 좋을 것 같다고 생각하게 되었다.
각 DTO가 명확한 책임을 가지게 하여, 가독성과 유지보수성을 향상시키는 방향이 좋은 방향! API 목적 = DTO 목적
- plus, DTO는 View에 맞춰 설계해야 한다.
- DTO는 전달 목적에 맞게 필요한 정보만 포함해야 한다.
- 실제 사용자에게 보이지 않는 데이터를 포함하면
- 응답 크기 증가, 클라이언트/프론트 코드가 혼란스러움, 유지보수 시 헷갈림
2️⃣ Entity에서 DTO 생성하는 방식
- Entity에서 DTO를 생성하는 방식
- 예시 코드
public record TestDto( String data1, String data2 ) { public static TestDto from(Entity entity) { return new TestDto(entity.getData1(), entity.getData2();) } }
- 장점
- 엔티티와 DTO간이 명확한 매핑
- 엔티티에서 DTO로 변환하는 로직이 한 곳에 집중되어 있어 매핑 로직이 명확함
- 일관성
- 항상 엔티티에서 DTO로 변환하는 방식이 일관되어 코트의 패턴이 일정
- 유지보수성
- 엔티티의 필드가 변경되면 DTO 변환 로직만 수정하면 되므로 유지보수가 상대적으로 쉬움
- 도메인 중심 설계
- 엔티티를 중심으로 데이터 흐름이 설계되어, DDD 방식에 더 적합
- 엔티티와 DTO간이 명확한 매핑
- 단점
- DTO와 엔티티 사이의 강한 결합
- DTO가 엔티티 구조에 의존하므로 결합도가 높다.
- 테스트 복잡성
- 엔티티없이 DTO를 테스트하기 어려움
- DTO와 엔티티 사이의 강한 결합
- 파라미터로 DTO를 직접 생성하는 방식
- 예시 코드
public record TestDto( String data1, String data2 ){ public static TestDto from( String data1, String data2 ) { return new TestDto(data1, data2); } }
- 장점
- 낮은 결합도
- DTO가 엔티티에 직접적으로 의존하지 않아 결합도 낮음
- 유연성
- 엔티티 외에도 다양항 소스에서 DTO를 생성할 수 있어 유연
- 테스트 용이성
- 엔티티 없이도 DTO 테스트 가능
- 서비스 계층의 자율성
- 서비스 계층에서 DTO생성 시 엔티티 변환 외에도 다양한 로직을 적용할 수 있음
- 엔티티 구조 변경에 영향 최소화
- 엔티티 내부 구조가 변경되어도 DTO 생성 로직은 서비스 계층에서만 수정 가능
- 낮은 결합도
- 단점
- 중복 코드 가능성
- 여러 곳에서 동일한 DTO를 생성하는 경우 중복 코드가 발생할 수 있음
- 일관성 유지 어려움
- 서로 다른 서비스에서 동일한 DTO를 다르게 생성할 수 있음
- 매핑 로직 분산
- 엔티티에서 DTO로 변환하는 로직이 여러 곳에 분산될 수 있음
- 중복 코드 가능성
3️⃣ @Valid
- 역할
- 해당 객체의 필드에 붙은
@Notnull
,@Size
,@Min
등 유효성 검증(Validation) 어노테이션을 실행하게 함
- 해당 객체의 필드에 붙은
- 사용 위치
@RequestBody
,@ModelAttribute
등에서 받은 객체의 값이 유효ㅏㄴ지 검사
- 예시
public record CreatePeerReviewRequest( @NotNull Long projectId, @Min(1) @Max(5) Integer technicalScore, ... ) {}
- 이렇게 DTO에 유효성 조건을 걸고
@Valid
를 붙이면, 조건을 만족하지 못할 경우 400 Bad Request 응답이 자동 발생