JPA에서 DTO 조회 - 컬렉션 최적화

Park sang woo·2024년 9월 3일
0

CS스터디

목록 보기
18/25

⚒️ JPA에서 DTO 직접 조회 (최적화 X)

ToOne 관계와 일대다 관계를 따로 분리해서 조회한 것 입니다.

/**
 * 컬렉션은 별도로 조회
 * Query: 루트 1번, 컬렉션 N 번
 * 단건 조회에서 많이 사용하는 방식
 * OrderQueryRepository는 엔티티가 아닌 특정 화면들의 핏한 쿼리들을 위한 것 (관심사가 분리됨)
 */
 public List<OrderQueryDto> findOrderQueryDtos() {
     //루트 조회(toOne 코드를 모두 한번에 조회)
     List<OrderQueryDto> result = findOrders();

     //루프를 돌면서 컬렉션 추가(추가 쿼리 실행)
     result.forEach(o -> {
         List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
         o.setOrderItems(orderItems);
    });
    return result;
}

/**
 * 1:N 관계(컬렉션)를 제외한 나머지를 한번에 조회
 */
private List<OrderQueryDto> findOrders() {
    return em.createQuery(
            "select new jpabook.jpashop.repository.order.query.OrderQueryDto(o.id, m.name, o.orderDate, o.status, d.address)" +
                    " from Order o" +
                    " join o.member m" +
                    " join o.delivery d", OrderQueryDto.class)
            .getResultList();
}

/**
 * 1:N 관계인 orderItems 조회
 */
private List<OrderItemQueryDto> findOrderItems(Long orderId) {
    return em.createQuery(
            "select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count)" +
                    " from OrderItem oi" +
                    " join oi.item i" +
                    " where oi.order.id = : orderId", OrderItemQueryDto.class)
            .setParameter("orderId", orderId)
            .getResultList();
}

QueryDsl로 바꾸면 이런 방식으로 됨.

private List<OrderItemQueryDto> findOrderItems(Long orderId) {
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);

    return queryFactory
            .select(new QOrderItemQueryDto(
                    orderItem.order.id,
                    item.name,
                    orderItem.orderPrice,
                    orderItem.count))
            .from(orderItem)
            .join(orderItem.item, item)
            .where(orderItem.order.id.eq(orderId))
            .fetch();
}

ToOne 관계는 조인해도 데이터 row 수가 증가하지 않는데 ToMany 관계는 조인하면 row 수가 증가합니다. (데이터 뻥튀기)
ToMany는 루프를 돌면서 채워넣어 OrderItem을 가지고 오고 set해서 반환해준 것.

하지만 이렇게 할 경우 Order 1번 쿼리, OrderItem이 N번 쿼리가 나가 N+1 문제가 발생합니다.






⚒️ 컬렉션 조회 (최적화)

컬렉션이 루프를 돌지 않고 한방에 가져오는 방법입니다.

/**
 * 최적화
 * Query: 루트 1번, 컬렉션 1번
 * 데이터를 한꺼번에 처리할 때 많이 사용하는 방식
 *
 */
public List<OrderQueryDto> findAllByDto_optimization() {

    // 루트 조회(toOne 코드를 모두 한번에 조회)
    List<OrderQueryDto> result = findOrders();

    // orderItem 컬렉션을 MAP 한방에 조회
    Map<Long, List<OrderItemQueryDto>> orderItemMap = findOrderItemMap(toOrderIds(result));

    // 루프를 돌면서 컬렉션 추가(추가 쿼리 실행X)
    result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));

    return result;
}

// OrderQueryDto인데 Long 형태로 바꾼 것 -> OrderId의 리스트로 만들기 위함
// findOrderItemMap에서 jpql에 in절 안에 파라미터 인자로 넣어주기 위함
private List<Long> toOrderIds(List<OrderQueryDto> result) {
    return result.stream()
            .map(o -> o.getOrderId())
            .collect(Collectors.toList());
}

// 
private Map<Long, List<OrderItemQueryDto>> findOrderItemMap(List<Long> orderIds) {
    List<OrderItemQueryDto> orderItems = em.createQuery(
            "select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count)" +
                    " from OrderItem oi" +
                    " join oi.item i" +
                    " where oi.order.id in :orderIds", OrderItemQueryDto.class)
            .setParameter("orderIds", orderIds)
            .getResultList();

    return orderItems.stream()
            .collect(Collectors.groupingBy(OrderItemQueryDto::getOrderId));
}

기존에는 where절에 order.id를 =을 통해 하나씩 가져왔지만 in을 활용해 한꺼번에 가져와 버린 것입니다.
또한 groupingBy를 사용해서 OrderId를 기준으로 Map으로 변형이 가능합니다. 그러면 키가 OrderId, 값은 OrderItemQueryDto가 됩니다.

기존 것과 차이점

  • 루프를 돌릴 때마다 query를 날렸는데 지금은 query를 한 번 날리고 메모리에 Map 형태로 가져와서 매칭해서 값을 세팅해준 것입니다.
  • 이 경우 Query가 총 2번 나갑니다. (Order 1개, OrderItem 1개)

querydsl이라면?

List<OrderItemQueryDto> orderItems = queryFactory
        .select(new QOrderItemQueryDto(orderItem.order.id, order.name, orderItem.orderPrice, orderItem.count))
        .from(orderItem)
        .join(orderItem.item, order)
        .where(orderItem.order.id.in(orderIds))
        .fetch();





⚒️ 플랫 데이터 최적화

이번에는 쿼리 한방으로만 해결하는 방법입니다.

public List<OrderFlatDto> findAllByDto_flat() {
    return em.createQuery(
            "select new jpabook.jpashop.repository.order.query.OrderFlatDto(o.id, m.name, o.orderDate, o.status, d.address, i.name, oi.orderPrice, oi.count)" +
                    " from Order o" +
                    " join o.member m" +
                    " join o.delivery d" +
                    " join o.orderItems oi" +
                    " join oi.item i", OrderFlatDto.class)
            .getResultList();
}

querydsl로 수정

public List<OrderFlatDto> findAllByDto_flat() {
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);

    return queryFactory
            .select(new QOrderFlatDto(o.id, m.name, o.orderDate, o.status, d.address, i.name, oi.orderPrice, oi.count))
            .from(o)
            .join(o.member, m)
            .join(o.delivery, d)
            .join(o.orderItems, oi)
            .join(oi.item, i)
            .fetch();
}
@GetMapping("/api/v6/orders")
public List<OrderQueryDto> ordersV6() {
    List<OrderFlatDto> flats = orderQueryRepository.findAllByDto_flat();

    return flats.stream()
            .collect(groupingBy(o -> new OrderQueryDto(o.getOrderId(), o.getName(), o.getOrderDate(), o.getOrderStatus(), o.getAddress()),
                    mapping(o -> new OrderItemQueryDto(o.getOrderId(), o.getItemName(), o.getOrderPrice(), o.getCount()), toList())
            )).entrySet().stream()
            .map(e -> new OrderQueryDto(e.getKey().getOrderId(), e.getKey().getName(), e.getKey().getOrderDate(), e.getKey().getOrderStatus(), e.getKey().getAddress(), e.getValue()))
            .collect(toList());
}

하지만 조인으로 인해 DB에서 애플리케이션에 전달하는 데이터에 중복 데이터가 추가되므로 상황에 따라 V5보다 느릴 수 있습니다. -> 데이터 양이 적다면 이게 더 빠를 수 있습니다.
또한 페이징도 불가능합니다.

profile
일상의 인연에 감사하라. 기적은 의외로 가까운 곳에 있을지도 모른다.

0개의 댓글