[TIL] DTO 나누는 기준, Entity에서 DTO 생성하는 방식, @Valid

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를 유지할 수 있는 이유
    1. 응답 목적이 분명히 다름
      • 예를 들어, PeerReviewDetailResponse는 리뷰 하나하나의 전체 정보를 보여줌
      • UserReviewSummaryResponse는 집계 결과만 보여줌
      • ➡️ DTO는 목적에 따라 나눠야지, 포함된 데이터 수가 적다고 합치면 안됨
    2. 확장성 고려
      • 오늘은 평균 점수와 코멘트만 보여주지만, 내일은 “피어리뷰 받은 날짜 리스트”, “리뷰 작성자 정보” 등이 추가될 수 있음
    3. API 유지보수 용이
      • 응답 스펙이 명확하게 고정된 하나의 DTO로 분리되어 있으면, 클라이언트와의 연동 시에도 변경 위험을 줄일 수 있음
      • 어떤 API는 summary만 주고, 어떤 API는 리뷰 detail 리스트를 따로 주는 등 조합이 자유로움
    4. 응답 크기 / 성능 최적화
      • 하나의 응답에 모든 리뷰 데이터를 다 담으면 리스트가 커짐 -> 느려질 수 있음
      • 필수정보만 담아 빠르고 가볍게 구현 가능

따라서 API 엔드포인트 별로, 응답 목적 별로 DTO를 나눠서 생성하는 것이 좋을 것 같다고 생각하게 되었다.

각 DTO가 명확한 책임을 가지게 하여, 가독성과 유지보수성을 향상시키는 방향이 좋은 방향! API 목적 = DTO 목적

  • plus, DTO는 View에 맞춰 설계해야 한다.
    • DTO는 전달 목적에 맞게 필요한 정보만 포함해야 한다.
    • 실제 사용자에게 보이지 않는 데이터를 포함하면
      • 응답 크기 증가, 클라이언트/프론트 코드가 혼란스러움, 유지보수 시 헷갈림

2️⃣ Entity에서 DTO 생성하는 방식

  1. Entity에서 DTO를 생성하는 방식
  • 예시 코드
    public record TestDto(
        String data1,
        String data2
    ) {
        public static TestDto from(Entity entity) {
            return new TestDto(entity.getData1(), entity.getData2();)
        }
    }
    
  • 장점
    1. 엔티티와 DTO간이 명확한 매핑
      • 엔티티에서 DTO로 변환하는 로직이 한 곳에 집중되어 있어 매핑 로직이 명확함
    2. 일관성
      • 항상 엔티티에서 DTO로 변환하는 방식이 일관되어 코트의 패턴이 일정
    3. 유지보수성
      • 엔티티의 필드가 변경되면 DTO 변환 로직만 수정하면 되므로 유지보수가 상대적으로 쉬움
    4. 도메인 중심 설계
      • 엔티티를 중심으로 데이터 흐름이 설계되어, DDD 방식에 더 적합
  • 단점
    1. DTO와 엔티티 사이의 강한 결합
      • DTO가 엔티티 구조에 의존하므로 결합도가 높다.
    2. 테스트 복잡성
      • 엔티티없이 DTO를 테스트하기 어려움
  1. 파라미터로 DTO를 직접 생성하는 방식
  • 예시 코드
    public record TestDto(
        String data1,
        String data2
    ){
        public static TestDto from(
            String data1,
            String data2
        ) {
            return new TestDto(data1, data2);
        }
    }
    
  • 장점
    1. 낮은 결합도
      • DTO가 엔티티에 직접적으로 의존하지 않아 결합도 낮음
    2. 유연성
      • 엔티티 외에도 다양항 소스에서 DTO를 생성할 수 있어 유연
    3. 테스트 용이성
      • 엔티티 없이도 DTO 테스트 가능
    4. 서비스 계층의 자율성
      • 서비스 계층에서 DTO생성 시 엔티티 변환 외에도 다양한 로직을 적용할 수 있음
    5. 엔티티 구조 변경에 영향 최소화
      • 엔티티 내부 구조가 변경되어도 DTO 생성 로직은 서비스 계층에서만 수정 가능
  • 단점
    1. 중복 코드 가능성
      • 여러 곳에서 동일한 DTO를 생성하는 경우 중복 코드가 발생할 수 있음
    2. 일관성 유지 어려움
      • 서로 다른 서비스에서 동일한 DTO를 다르게 생성할 수 있음
    3. 매핑 로직 분산
      • 엔티티에서 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 응답이 자동 발생