[JpaTest] DTO 설계와 계층 간 데이터 변환

y001·2025년 1월 24일
post-thumbnail

1. 도입: DTO(Data Transfer Object) 설계의 중요성

소프트웨어 개발에서 DTO(Data Transfer Object)는 데이터를 구조화하여 계층 간에 전달하는 데 중요한 역할을 합니다. 하지만 DTO를 적절히 설계하지 않으면, 코드 유지보수성과 확장성이 저하될 수 있습니다.


2. AS-IS 구조: 현재의 문제점

AS-IS 구조에서는 다음과 같은 문제가 발생할 수 있습니다:

  1. Controller와 Service 간 강한 의존성

    • Controller에서 사용하는 DTO를 Service 계층에서도 그대로 사용합니다. 이는 Controller 변경이 Service 계층에 영향을 미치는 강한 결합을 초래합니다.
  2. DTO 변환의 책임 불명확

    • DTO 변환 로직이 한 계층에 과도하게 집중되거나 분산되어 코드의 일관성이 떨어질 수 있습니다.
  3. 확장성 저하

    • 화면단 요구사항이 변경되면 Service와 Repository 계층까지 영향을 미쳐 수정 비용이 증가합니다.
AS-IS 구조의 흐름
[Servlet] → [ControllerRequest] → [Service] → [Entity] → [Repository]
  • Servlet에서 요청 데이터를 ControllerRequest로 변환하고, Service에서 추가 변환 없이 그대로 처리합니다.
AS-IS 구조 코드 예제

1. Controller

@PostMapping("/api/v1/parking-tickets/new")
public ApiResponse<ParkingTicketResponse> createParkingTicket(@Valid @RequestBody ParkingTicketCreateRequest request) {
    return ApiResponse.ok(parkingService.createParkingTicket(request));
}
  • Controller에서 ParkingTicketCreateRequest를 Service로 바로 전달합니다.

2. Controller용 DTO

@NoArgsConstructor
@Getter
public class ParkingTicketCreateRequest {

    @NotNull(message = "차량 번호는 필수입니다.")
    private String vehicleNumber;

    @NotNull(message = "입차 시간은 필수입니다.")
    private LocalDateTime entryTime;

    @Builder
    public ParkingTicketCreateRequest(String vehicleNumber, LocalDateTime entryTime) {
        this.vehicleNumber = vehicleNumber;
        this.entryTime = entryTime;
    }
}
  • Controller와 Service에서 동일한 DTO를 사용하여 계층 간 결합도를 높입니다.

3. TO-BE 구조: 개선된 설계

TO-BE 구조는 다음과 같은 방식으로 문제를 해결합니다:

  1. 계층 간 독립성 확보

    • Controller는 요청 데이터를 처리하고, Service에 적합한 DTO로 변환한 뒤 전달합니다.
  2. DTO 변환 책임 명확화

    • 변환 로직을 Controller에서 처리하여 각 계층의 책임을 명확히 합니다.
  3. 확장성과 유지보수성 향상

    • 화면단 요구사항이 변경되더라도 Controller만 수정하면 되므로, 다른 계층에는 영향을 주지 않습니다.
TO-BE 구조의 흐름
[Servlet] → [ControllerRequest] → [ServiceRequest] → [Service] → [Entity] → [Repository]
  • Controller에서 ServiceRequest로 변환한 뒤 Service로 전달합니다.
TO-BE 구조 코드 예제

1. Controller

@PostMapping("/api/v1/parking-tickets/new")
public ApiResponse<ParkingTicketResponse> createParkingTicket(@Valid @RequestBody ParkingTicketCreateRequest request) {
    return ApiResponse.ok(parkingService.createParkingTicket(request.toServiceRequest()));
}
  • 요청 데이터를 검증한 후, Service용 DTO로 변환하여 Service 계층으로 전달합니다.

2. Controller용 DTO

@NoArgsConstructor
@Getter
public class ParkingTicketCreateRequest {

    @NotNull(message = "차량 번호는 필수입니다.")
    private String vehicleNumber;

    @NotNull(message = "입차 시간은 필수입니다.")
    private LocalDateTime entryTime;

    @Builder
    public ParkingTicketCreateRequest(String vehicleNumber, LocalDateTime entryTime) {
        this.vehicleNumber = vehicleNumber;
        this.entryTime = entryTime;
    }

    public ParkingTicketCreateServiceRequest toServiceRequest() {
        return ParkingTicketCreateServiceRequest.builder()
                .vehicleNumber(vehicleNumber)
                .entryTime(entryTime)
                .build();
    }
}
  • Service 계층으로 데이터를 전달하기 위해 ServiceRequest로 변환합니다.

3. Service용 DTO

@Getter
public class ParkingTicketCreateServiceRequest {

    private String vehicleNumber;
    private LocalDateTime entryTime;

    @Builder
    public ParkingTicketCreateServiceRequest(String vehicleNumber, LocalDateTime entryTime) {
        this.vehicleNumber = vehicleNumber;
        this.entryTime = entryTime;
    }

    public ParkingTicket toEntity(String ticketNumber) {
        return ParkingTicket.builder()
                .ticketNumber(ticketNumber)
                .vehicleNumber(vehicleNumber)
                .entryTime(entryTime)
                .build();
    }
}
  • Service 계층에서 사용할 데이터를 정의하며, 도메인 모델(Entity)로 변환하는 메서드를 제공합니다.

4. 결론

AS-IS 구조에서는 Controller와 Service 간 강한 결합과 DTO 변환 책임의 불명확성이 문제였습니다. TO-BE 구조에서는 이를 해결하기 위해 DTO를 분리하고 각 계층의 책임을 명확히 정의했습니다.

📌 이 글은 TDD 강의를 학습한 내용을 바탕으로 재구성하였습니다. 문제가 되는 부분이 있다면 수정하겠습니다.

0개의 댓글