컬렉션이 있을 때 어떻게 DTO로 조회하는지 알아보도록 하겠습니다.

주문 조회 V4: JPA에서 DTO 직접 조회

@RestController
@RequiredArgsConstructor
public class OrderApiController {

    private final OrderRepository orderRepository;
    private final OrderQueryRepository orderQueryRepository;

    @GetMapping("/api/v4/orders")
    public List<OrderQueryDto> ordersV4() {
        return orderQueryRepository.findOrderQueryDtos();
    }

}
@Data
public class OrderQueryDto {

    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;
    private List<OrderItemQueryDto> orderItems;

    public OrderQueryDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address) {
        this.orderId = orderId;
        this.name = name;
        this.orderDate = orderDate;
        this.orderStatus = orderStatus;
        this.address = address;
    }

}
@Data
public class OrderItemQueryDto {

    @JsonIgnore
    private Long orderId;
    private String itemName;
    private int orderPrice;
    private int count;

    public OrderItemQueryDto(Long orderId, String itemName, int orderPrice, int count) {
        this.orderId = orderId;
        this.itemName = itemName;
        this.orderPrice = orderPrice;
        this.count = count;
    }

}
package jpabook.jpashop.repository.order.query; //...1

@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {

    private final EntityManager em;

    public List<OrderQueryDto> findOrderQueryDtos() {
        List<OrderQueryDto> results = findOrders();
        results.forEach(o -> {
            List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
            o.setOrderItems(orderItems);
        });
        return results;
    }

		//...2
    private List<OrderItemQueryDto> findOrderItems(Long orderId) {
        return em.createQuery(
						           //...3
                        "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();
    }
    
    private List<OrderQueryDto> findOrders() {
        return em.createQuery(
								        //...2
                        "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. OrderQueryRepository 와 OrderRepository 의 패키지를 분리했습니다.

    즉, 관심사 분리하기 위해 패키지를 분리했다고 보면 됩니다. → 자세히

  2. jpql 을 작성하더라도 컬렉션을 바로 넣을 수 없습니다.

    사실 jpql 도 sql 을 사용하는 것과 비슷합니다. 그래서 데이터를 플랫하게 한 줄로 밖에 못 넣습니다.

    그런데 OrderItem 같은 경우는 일대다(1:N)라서 데이터가 뻥튀기 되기 때문에 바로 DTO에 넣을 수 없습니다.

    그래서 OrderItem 에 대한 부분은 따로 쿼리를 작성해야합니다. 그래서 루프를 돌면서 쿼리를 날리고 OrderItem 에 대한 값을 직접 넣어주고 있습니다.

  3. OrderItem 에 대한 쿼리를 보면 orderId 를 가져오도록 oi.order.id 로 작성했습니다. 이러면 OrderItem 은 Order 를 참조하는게 아니라 DB 입장에서 orderId 를 FK 로 가지고 있기 때문에 바로 넣어줍니다.

    물론 JSON 으로 응답할 때는 필요 없는 데이터라서 @JsonIgnore 를 했습니다.

실행을 하면 원하는대로 값이 노출 됩니다.

[
    {
        "orderId": 4,
        "name": "userA",
        "orderDate": "2024-04-18T16:43:05.649628",
        "orderStatus": "ORDER",
        "address": {
            "city": "서울",
            "street": "1",
            "zipcode": "1111"
        },
        "orderItems": [
            {
                "itemName": "JPA1 BOOK",
                "orderPrice": 10000,
                "count": 1
            },
            {
                "itemName": "JPA2 BOOK",
                "orderPrice": 20000,
                "count": 2
            }
        ]
    },
    {
        "orderId": 11,
        "name": "userB",
        "orderDate": "2024-04-18T16:43:05.770806",
        "orderStatus": "ORDER",
        "address": {
            "city": "진주",
            "street": "2",
            "zipcode": "2222"
        },
        "orderItems": [
            {
                "itemName": "SPRING1 BOOK",
                "orderPrice": 20000,
                "count": 3
            },
            {
                "itemName": "SPRING2 BOOK",
                "orderPrice": 40000,
                "count": 4
            }
        ]
    }
]

컬렉션을 DTO로 직접 조회할 때 N + 1 문제 발생