Project/배달 주문 관리 플랫폼

[Spring Boot] 배달 플랫폼 배송지 도메인 설계: Request / Response DTO 패턴

Kr1 2026. 5. 4. 00:57
  • 배송지 도메인에서 클라이언트의 요청(Request)을 받고 응답(Response)을 내려주기 위해 작성한 DTO 구조를 리뷰해보겠습니다.

 

 

💻 실제 코드 (Request & Response DTO)

요청 DTO: ReqCreateAddressDtoV1.java
@Getter
@Schema(description = "배송지 등록 요청 객체")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class ReqCreateAddressDtoV1 {

    @Schema(description = "별칭", example = "우리집")
    private String alias;

    @Schema(description = "주소", example = "서울특별시 종로구 세종대로 172")
    @NotBlank(message = "배송지 주소는 필수 입력 사항입니다.")
    private String address;

    // ... (detail, zipCode 등 생략)

    public AddressEntity toEntity(UUID userId) {
        return AddressEntity.builder()
                .userId(userId)
                .alias(this.alias)
                .address(this.address)
                .detail(this.detail)
                .zipCode(this.zipCode)
                .isDefault(this.isDefault != null ? this.isDefault : false)
                // isDeleted는 부모(BaseSoftDeleteEntity)가 자동으로 false로 설정.
                .build();
    }
}
응답 DTO: ResGetAddressDtoV1.java
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class ResGetAddressDtoV1 {
    private UUID addressId;
    private UUID userId; // 유저 ID 필드 추가
    private String alias;
    private String address;
    // ... 생략 ...
    private String message;

    // 메시지 없이 변환할 때 (기본 호출)
    public static ResGetAddressDtoV1 from(AddressEntity entity) {
        return from(entity, null); // 중복 제거를 위해 오버로딩 활용
    }

    // 메시지를 포함하여 변환할 때
    public static ResGetAddressDtoV1 from(AddressEntity entity, String message) {
        return ResGetAddressDtoV1.builder()
                .addressId(entity.getId())
                .userId(entity.getUserId())
                .alias(entity.getAlias())
                .address(entity.getAddress())
                // ... 생략 ...
                .message(message)
                .build();
    }
}

 

🤔 왜 이렇게 짰는가? (설계 의도와 이유)

왜 Entity를 바로 쓰지 않고 DTO를 따로 만들었을까?

  • 가장 중요한 아키텍처 원칙 중 하나입니다. 만약 클라이언트에게 AddressEntity를 그대로 반환하다면,
    • Entitiy에 비밀번호나 숨겨야 할 데이터가 추가되면 API 응답으로 노출될 수 있습니다.
    • 클라이언트가 화면에 그리기 위해 필요한 데이터(예: 성공 message)를 엔티티에 억지로 추가해야 합니다.
    • 따라서 뷰(View) 로직과 DB 로직을 철저히 분리하기 위해  Request/Response 용도의 DTO를 분리하여 만들어야 합니다.

@NotBlank 어노테이션의 역할 (Validation)

  • 요청 DTO에 @NotBlank(message = "배송지 주소는 필수 입력 사항입니다.")가 있습니다.
  • 이 코드는 컨트롤러에 도달하기 전, 스프링의 스프링의 Validation (유효성 검증) 매커니즘을 통해 클라이언트가 빈 값을 보냈는지 미리 차단해 줍니다. 서비스 로직 안에서 일일이 if(address == null) 체크를 하는 수고를 덜어주는 핵심 문법입니다.

toEntity()와 from() 메서드를 DTO 안에 둔 이유

  • toEntity(): 클라이언트로부터 받은 DTO를 DB에 저장할 엔티티로 변환합니다.
  • from(): DB에서 꺼낸 엔티티를 클라이언트에게 줄 응답 DTO로 변환합니다. (Factory Method 패턴)
이 변환 로직을 Service 계층에 길게 늘어놓으면 서비스 코드가 너무 지저분해집니다. 변환 책임을 DTO 스스로에게 부여(객체 지향적)하여 서비스 로직의 가독성을 높였습니다.

 

💡 정리 

  • 엄격한 계층 분리: API 스펙과 DB 스키마가 서로 결합(Coupling)되는 것을 막기 위해 Entity와 DTO를 엄격히 분리했습니다. 이를 통해 Entity가 변경되어도 API 응답 규격이 깨지지 않도록 설계했습니다.
  • 객체 지향적 변환 로직: 엔티티와 DTO 간의 변환 책임을 Service가 아닌 DTO의 팩토리 메서드(from, toEntity)에 위임하여 코드의 응집도를 높이고 Service 코드를 깔끔하게 유지했습니다.
  • 안전한 입력 검증: "jakarta.validation(@NotBlank)을 활용하여 컨트롤러 진입 전 방어 로직을 구축, 불필요한 서비스 계층 실행을 차단했습니다.