JPA에서 DTO 내의 컬렉션 처리를 설명하는 두 가지 방법과 각각의 장단점에 대해 알아본다

방법 1: DTO + 컬렉션 분리 조회 후 매핑

주요 흐름

  • 부모 OrderDto와 자식 OrderItemDto를 각각 조회합니다.
  • 부모 DTO의 ID를 기준으로 자식 DTO를 IN 절을 사용해 조회합니다.
  • 조회한 자식 데이터를 Map으로 변환하여 부모 DTO와 매핑합니다.

구현 과정

  • OrderDto 리스트를 조회
List<OrderDto> result = findOrders(); // order 조회 처리
  • 부모 DTO의 ID 목록 생성
List<Long> orderIds = result.stream()
    .map(OrderDto::getOrderId)
    .collect(Collectors.toList());
  • 자식 DTO 조회 (IN 절 사용)
List<OrderItemDto> orderItemDtos = em.createQuery(
    "SELECT new com.example.dto.OrderItemDto(...) " +
    "FROM OrderItem oi " +
    "WHERE oi.orderId IN :orderIds", OrderItemDto.class)
    .setParameter("orderIds", orderIds)
    .getResultList();

자식 데이터를 Map으로 변환:

Map<Long, List<OrderItemDto>> orderItemMap = orderItemDtos.stream()
    .collect(Collectors.groupingBy(OrderItemDto::getOrderId));

부모 DTO와 자식 DTO 매핑

result.forEach(order -> order.setOrderItems(orderItemMap.get(order.getOrderId())));

장점

  • 효율성: 부모-자식을 개별적으로 쿼리하므로, 페이징 처리가 가능함
  • 유연성: 필요한 데이터만 조회하여 처리할 수 있다.

단점

  • 추가 로직 필요: 부모와 자식을 수동으로 매핑해야 하므로 코드가 다소 복잡함
  • 다중 쿼리 발생: 부모와 자식 데이터를 각각 조회하므로 쿼리 호출이 많아질 수 있다.

방법 2: Flat DTO 사용

주요 흐름

  • 부모와 자식 데이터를 한 번의 쿼리로 조회하고, 이를 Flat 형태의 DTO로 매핑.
  • Flat DTO에서 부모와 자식을 분리하여 최종적인 DTO로 재구성.

**구현과정 **

  • Flat DTO 정의
public class OrderFlatDto {
    private Long orderId;
    private String orderName;
    private String orderItemName;
    private int orderItemPrice;
}
  • 쿼리로 Flat DTO 조회
List<OrderFlatDto> flatResult = em.createQuery(
    "SELECT new com.example.dto.OrderFlatDto(o.orderId, o.name, i.name, i.price) " +
    "FROM Order o " +
    "JOIN o.orderItems i", OrderFlatDto.class)
    .getResultList();

  • Flat 데이터를 그룹핑하여 최종 DTO로 변환
Map<Long, List<OrderFlatDto>> flatMap = flatResult.stream()
    .collect(Collectors.groupingBy(OrderFlatDto::getOrderId));

List<OrderDto> result = flatMap.entrySet().stream()
    .map(entry -> {
        Long orderId = entry.getKey();
        List<OrderFlatDto> flatItems = entry.getValue();
        
        OrderDto order = new OrderDto(orderId, flatItems.get(0).getOrderName());
        List<OrderItemDto> items = flatItems.stream()
            .map(flat -> new OrderItemDto(flat.getOrderItemName(), flat.getOrderItemPrice()))
            .collect(Collectors.toList());
        order.setOrderItems(items);
        return order;
    })
    .collect(Collectors.toList());

장점

  • 단일 쿼리: 부모와 자식 데이터를 한 번에 조회하여 데이터베이스 호출이 적다.
  • 간결한 쿼리: 단일 쿼리로 필요한 데이터를 모두 가져올 수 있다.

단점

  • 페이징 불가: 부모-자식 관계의 데이터가 조인되면서 결과가 폭발적으로 증가하므로 페이징이 어렵다
  • 복잡한 변환 작업: Flat 데이터를 부모-자식 구조로 변환하는 추가 로직이 필요하다.

결론

  • 방법 1페이징이나 대규모 데이터 처리가 필요한 경우 적합하다. 비록 코드가 다소 복잡하지만, 데이터 효율성과 유연성 면에서 유리함.
  • 방법 2는 작은 데이터셋을 다룰 때 적합. 쿼리 호출 횟수를 최소화하고 데이터베이스 작업을 간소화할 수 있지만, 변환 과정이 추가되어 관리가 어렵다

실제 프로젝트에서는 데이터 규모와 성능 요구 사항을 고려하여 적합한 방식을 선택하는 것이 중요하다.