본문 바로가기
Back-end/JPA 개념

API 조회 성능 최적화 - 컬렉션(DTO 직접 조회)

by devraphy 2022. 6. 4.

0. 개요

- 이번 포스팅에서는 DTO를 직접 사용하여 컬렉션을 조회하는 방식에 대해서 알아보자.

 

 

1. DTO 직접 조회 준비

a) DTO 직접 조회의 의미

- 단일 조회 시 DTO를 직접 사용한 조회 방법과 동일하게 컬렉션도 DTO를 사용하여 직접 조회할 수 있다.

- DTO 직접 조회 방식의 핵심은 Entity를 전혀 사용하지 않고 오직 DTO만을 이용하여 데이터를 받아오는 것이다.

- 즉, Entity를 조회하고 그 데이터를 이용하여 DTO를 생성 및 반환하는 작업이다. 

 

- 언뜻 보면 DTO 변환 방식과 특별한 차이점을 못 느낄 수도 있다.

- 그러나 DTO 직접 조회 방식은 API 메서드에서 오직 DTO만을 사용한다는 것이 특징이다. 

 

 

b) 준비 작업 확인

- 우선 DTO 변환을 이용한 조회 메서드를 기반으로 DTO 직접 조회에 필요한 것들을 확인해보자. 

- DTO 변환 조회 메서드의 코드는 다음과 같다.

 

@GetMapping("/api/v3/orders")
    public List<OrderDto> ordersV3() {
        List<Order> orders = orderRepository.findAllWithItem();
        List<OrderDto> result = orders.stream()
                .map(o -> new OrderDto(o)).collect(Collectors.toList());
        return result;
}

 

- 위의 코드를 하나씩 분석해보자.

- 우선, orderRepository의 메서드를 사용하여 Order Entity를 찾아왔다.

- stream() 메서드로 찾아온 Order 객체를 OrderDTO로 변환하였다.

 

- 이 과정에서 필요한 2가지를 알 수 있다. 

  → DTO를 반환하는 Repository가 필요하다. 

  → Repository에서 반환할 DTO가 필요하다. 

 

- 그렇다면 이 2가지를 준비하면서 또 무엇이 필요한지 차근차근 만들어가 보자. 

 

 

2. DTO 반환 Repository 생성

a) DTO 생성

- 우선 Repository를 만들기 전에, Repository에서 반환할 DTO가 필요하다.

- 그렇다면 기존에 사용하던 OrderDTO의 형태를 확인해보자, 

- OrderDTO는 다음과 같이 구성되었다. 

 

@Data
public class OrderDto {

    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;
    private List<OrderItemDto> orderItems;
    
    // 생성자 생략
}

 

- OrderDto의 필드를 살펴보자.

- 내부에 컬렉션인 orderItems가 존재한다. 컬렉션 필드의 자료형 또한 DTO로 만들어 줘야 한다. 

- 다만, 여기서 주의할 점은 DTO를 이용해 직접 조회할 때 컬렉션 필드를 함께 사용해서는 안된다는 점이다.

 

- 컬렉션 필드를 조회에 사용하면 Join이 필수다.

- 그러나 일대다(OneToMany) 관계를 Join 하면 데이터 뻥튀기가 발생한다.

- 그러므로 다음과 같이 DTO를 구성할 수 있다. 

 

@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;
    }
}

 

- 위의 DTO의 핵심은 생성자에 있다.

- 생성자를 살펴보면 컬렉션 필드인 orderItems가 생략되어 있다.

- 데이터 뻥튀기 문제를 해결하기 위해, 후처리 과정으로 직접 주입하는 방식을 사용할 예정이다. 

 

 

b) 컬렉션 필드를 위한 DTO 생성

- 컬렉션 필드의 자료형은 다른 Entity를 참조한다. 

- 원래는 OrderItems를 참조하지만, 이는 Entity를 노출하는 방식이므로 DTO를 만들어준다.

 

@Data
public class OrderItemQueryDto {

    @JsonIgnore // 화면에 데이터를 뿌리기 위한 DTO 이므로 orderId 값은 중요하지 않음.
    private Long orderId;
    private String itemName;
    private int orderPrice;
    private int count;
    
    // 생성자 생략
}

 

 

c) DTO를 이용한 조회 

- OrderQueryDto를 이용하여 DB로부터 조회 결과를 받아온다.

- 이를 위해서 다음과 같은 JPQL 메서드를 사용한다.

 

// Repository에 작성
private List<OrderQueryDto> findOrders() {
        // OrderQueryDto의 생성자에서 OrderItems 라는 컬렉션 필드는 제외되었다.
        // 그 이유는 컬렉션이기 때문이다.
        // DB의 관점에서는 한줄의 데이터가 삽입되어야 한다.
        // 그러나 컬렉션이 포함되는 순간, 일대다 관계이므로 데이터의 수가 뻥튀기 된다.
        // 그러므로 쿼리를 작성하는 부분에서는 데이터 뻥튀기를 고려하여 다음과 같이 작성한다.
    return em.createQuery(
        "select new jpabook.jpabook.dto.OrderQueryDto(o.id, m.name, o.orderDate, o.status, d.adderss) " +
        "from Order o " +
        "join o.member m " +
        "join o.delivery d", OrderQueryDto.class).getResultList();
}

 

- 위의 JPQL을 이용하여 DB로부터 직접 OrderQueryDto에 데이터를 받아온다. 

- 그러나 아직 끝이 아니다. 컬렉션 필드인 OrderItems의 값이 빠져있기 때문이다.

 

 

d) 컬렉션 필드 채워 넣기 

- 다음과 같은 방식으로 OrderQueryDto 객체에 OrderItems 값을 채워 넣는다. 

- 이를 위해서 DB로부터 OrderItem의 값을 조회해야 하는데, 이를 findOrderItems() 메서드에서 수행한다. 

 

// Repository에 작성
public List<OrderQueryDto> findOrderQueryDtos() {
        
    // result는 컬렉션 필드(orderItems)가 비어있는 상태
    List<OrderQueryDto> result = findOrders();

    // 컬렉션 필드를 직접 채우는 과정
    result.forEach(o -> { 
        List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());            
        o.setOrderItems(orderItems);
    });

    return result;
}

 

- findOrderItems() 메서드 내부에서는 다음과 같은 JPQL을 이용하여 OrderItem의 값을 받아온다. 

- 해당 JPQL에서 반환된 조회 결과는 OrderItemQueryDto에 저장된다. 

 

// Repository에 작성
private List<OrderItemQueryDto> findOrderItems(Long orderId) {
    // 컬렉션 필드의 값을 조회하는 JPQL 
    return em.createQuery(
        "select new jpabook.jpabook.dto.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();
}

 

- 위의 findOrderQueryDto() 메서드는 다음과 같은 과정을 거친다. 

  → OrderQueryDto를 사용하여 DB에서 조회된 데이터를 받는다. (이때 컬렉션 필드는 조회 대상에서 제외된다.) 

  → OrderQueryDto 객체에는 컬렉션 필드인 OrderItems 값이 비어있는 상황이다.

 

  → 반복문으로 OrderQueryDto 객체를 탐색하며 findOrderItems() 메서드를 호출한다.

  → findOrderItems() 메서드는 OrderQueryDto 객체의 Id 값을 이용하여 DB로부터 OrderItem의 값을 조회한다.

 

  → 조회된 OrderItem의 값은 OrderItemQueryDto 객체에 담기며, 이를 orderItems라고 명명하였다. 

  → 마지막으로 orderItems를 OrderItemQueryDto에 할당한다.

 

 

e) Repository 전체 코드

- 여기까지 DTO 직접 조회를 구현하였다.

- Repository와 이를 호출하는 Controller의 전체적인 코드는 다음과 같이 구성된다. 

 

@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {

    private final EntityManager em;

    public List<OrderQueryDto> findOrderQueryDtos() {

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

    private List<OrderQueryDto> findOrders() {
        return em.createQuery(
            "select new jpabook.jpabook.dto.OrderQueryDto(o.id, m.name, o.orderDate, o.status, d.adderss) " +
            "from Order o " +
            "join o.member m " +
            "join o.delivery d", OrderQueryDto.class).getResultList();
    }

    private List<OrderItemQueryDto> findOrderItems(Long orderId) {
        return em.createQuery(
            "select new jpabook.jpabook.dto.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();

    }
}

 

- Controller에서는 다음과 같이 OrderQueryRepository를 이용한다. 

 

@RestController
@RequiredArgsConstructor
public class OrderApiController {

    private final OrderQueryRepository orderQueryRepository;

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

 

 

3. N + 1 문제

a) 쿼리 발행 지점 확인하기

- 다음 코드를 보자.

 

// Repository에 작성
public List<OrderQueryDto> findOrderQueryDtos() {
        
    // result는 컬렉션 필드(orderItems)가 비어있는 상태
    List<OrderQueryDto> result = findOrders();

    // 컬렉션 필드를 직접 채우는 과정
    result.forEach(o -> { 
        List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());            
        o.setOrderItems(orderItems);
    });

    return result;
}

 

- 위의 코드는 DB로부터 받아온 OrderQueryDto 객체에 비어있는 OrderItem 객체를 채워 넣는 과정을 거친다.

- findOrderQueryDtos() 메서드를 실행하면 총 몇 번의 쿼리가 발행될지 생각해보자. 

 

- 우선 findOrders() 메서드에서 쿼리가 1회 발행된다.

- 그리고 findOrderItems() 메서드에서 쿼리가 N번 발행된다.

- findOrderItems() 메서드는 반복문이 돌아가는 만큼 실행되기 때문이다.

 

- 즉, 결과적으로 N + 1 문제가 발생한다.

 

 

b) N + 1 문제의 원인 

- N + 1 문제의 원인은 JPQL의 WHERE 절을 살펴보면 쉽게 찾을 수 있다.

- 대부분 N + 1 문제는 WHERE 절에서 비교 또는 검색 조건이 하나씩 처리되기 때문에 발생한다.

- 다음 예시를 보면 쉽게 이해할 것이다. 

 

 

- 위 사진을 살펴보자.

- 빨간 줄이 그어진 WHERE 절을 보면 비교대상(= orderId)이 하나뿐이다.

- 즉, 하나의 쿼리마다 비교 대상이 하나씩이므로 다수(= 컬렉션 필드)를 조회하는 경우에는

  위의 쿼리를 반복적으로 실행할 수밖에 없다. 

- 이처럼 비교 또는 검색 조건이 하나만 사용되는 경우, 컬렉션의 개수만큼 쿼리가 발행되고 이는 N + 1 문제가 된다.

 

 

c) N + 1 문제의 해결법(IN 키워드)

- 이 문제의 해결책은 비교 또는 검색 조건을 여러 개 담을 수 있는 쿼리를 작성하는 것이다.

- 즉, IN 키워드를 사용한다.

- 다음 쿼리처럼 말이다. 

 

public List<OrderQueryDto> findAllByDto() {
    List<OrderQueryDto> result = findOrders();
    List<Long> orderIds = result.stream().map(o -> o.getOrderId()).collect(Collectors.toList());

    List<OrderItemQueryDto> orderItems = em.createQuery(
            "select new jpabook.jpabook.dto.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();

    // orderItems를 Map으로 변환 (사용하기 쉽게하기 위함)
    Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream()
            .collect(Collectors.groupingBy(orderItemQueryDto -> orderItemQueryDto.getOrderId()));

    // result의 orderItems 필드를 채우는 과정
    result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));

    return result;
}

 

- 위의 예시에서 JPQL 부분을 살펴보자.

- in 키워드를 사용하고 ordersIds라는 매개변수를 받는다. 

- ordersIds는 Long 타입의 List 자료구조로, 다수의 Id 값을 가지고 있다. 

- 이처럼 IN 키워드를 사용하여 N + 1 문제를 해결할 수 있다.

 

 

d) 총 몇 번의 쿼리가 발행될까?

- 위의 findAllByDto() 메서드는 총 2번의 쿼리를 발행한다.

- findOrders()에서 1번, orderItems를 찾아오는 JPQL에서 1번이 발행된다. 

- 이마저도 단 한 번의 쿼리 발행으로 해결할 수 있는 방법은 없을까? 

 

 

4. DTO 직접 조회를 한방 쿼리로

a) 2번의 쿼리가 발생한 이유

- 위에서 작성한 findAllByDto() 메서드는 총 2번의 쿼리를 발행한다.

 

- 이 2번의 쿼리 발행 시점은 다음과 같다. 

    → OrderQueryDto로 Order 테이블을 조회할 때 (컬렉션 필드인 orderItems는 비어있는 상태) 

    → 컬렉션 필드(orderItems)의 값을 OrderItem 테이블에서 조회할 때

 

- 당연하지만 2번의 JPQL이 사용되기 때문에 쿼리가 2번 발행되는 것이다.

- 즉, 컬렉션 필드(orderItems)의 값을 채워 넣는 과정에서 2개의 JPQL이 사용되기 때문이다.

 

- 그렇다면 이 과정을 하나의 JPQL로 처리하면 1번의 쿼리 발행으로 해결할 수 있지 않을까?

 

 

b) 1단계 - DTO 생성

- 컬렉션 필드의 값을 채워 넣는 과정을 없애기 위해서 새로운 DTO가 필요하다.  

- 해당 DTO는 다음과 같이 구성한다.

 

@Data
public class OrderFlatDto {

    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;

    // 컬렉션 필드 대신 컬렉션 필드의 속성으로 구성
    private String itemName;
    private int orderPrice;
    private int count;
    
    // 생성자 생략
}

 

- 위의 코드를 살펴보자.

- 컬렉션 필드를 사용하지 않고, 컬렉션 필드의 속성을 개별 필드로 생성하였다.

- 이와 같은 형태로 구성하는 이유는 일대다 JOIN으로 발생하는 중복 데이터를 모두 받아오기 위해서다. 

 

 

c) 2단계 - JPQL 작성

- 위의 DTO를 이용하여 다음과 같이 JPQL 메서드를 Repository에 작성한다.

 

 public List<OrderFlatDto> findAllByDtoFlat() {
    return em.createQuery(
        "select new jpabook.jpabook.dto.OrderFlatDto(o.id, m.name, o.orderDate, o.status, " +
            "d.adderss, 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();
}

 

- 위의 JPQL에서 알 수 있듯이, o.orderItems를 JOIN 하였다. 

- 일대다 JOIN을 하였으므로 데이터 중복(= 데이터 뻥튀기)이 발생할 수밖에 없는 상황이다.

- 위의 JPQL을 실행해보면 다음과 같은 결과를 반환한다. 

 

 

- 위의 결과 데이터는 PK(= id) 값이 동일한 객체가 2개씩 있으므로, 영속성 Context의 측면에서 중복 객체다.

- 즉, 해당 JPQL은 중복 데이터를 반환하는 것이다. (이는 일대다 JOIN으로 필수 불가결하다.)

- 그러므로 동일한 PK(= id) 값을 가진 객체끼리 묶어주는 작업을 수행해야 한다. 

 

 

d) 3 단계 - 중복 객체 grouping

- 중복 객체를 묶어줄 뿐만 아니라, 기존의 컬렉션 필드(= orderItems)를 되돌려놔야 한다.

- 그러므로 다음과 같은 과정을 필요로 한다. 

  → 주문 정보를 이용하여 OrderQueryDto를 만든다. (컬렉션 필드의 값은 비어있는 상태) 

  → 기존의 컬렉션 필드의 속성을 이용하여 OrderItemQueryDto로 만든다. 

  → 비어있는 OrderQueryDto의 컬렉션 필드에 OrderItemQueryDto를 넣어준다. 

 

- 무슨 말인지 이해가 잘 가지 않을 것이다.

- 다음 후처리 과정의 코드를 보면서 이해해보자. 

 

@RestController
@RequiredArgsConstructor
public class OrderApiController {

  private final OrderRepository orderRepository;
  private final OrderQueryRepository orderQueryRepository;
  
  
  @GetMapping("/api/v6/orders")
    public List<OrderQueryDto> orderV6 () {
    
    	List<OrderFlatDto> flats = orderQueryRepository.findAllByDtoFlat();
        
        return flats.stream()
            // stream()을 이용하여 OrderFlatDto의 일부를 OrderQueryDto로 변환
        	.collect(groupingBy(o -> new OrderQueryDto(o.getOrderId(), o.getName(),
                        o.getOrderDate(), o.getOrderStatus(), o.getAddress()),
                // stream()을 이용하여 OrderFlatDto의 일부를 OrderItemQueryDto로 변환
                mapping(o -> new OrderItemQueryDto(o.getOrderId(), o.getItemName(),
                            o.getOrderPrice(), o.getCount()), toList())
            )).entrySet().stream()
            // 최종적으로 OrderQueryDto와 OrderItemQueryDto를 이용하여 OrderQueryDto 생성
            .map(e -> new OrderQueryDto(e.getKey().getOrderId(),
                e.getKey().getName(), e.getKey().getOrderDate(),
                e.getKey().getOrderStatus(), e.getKey().getAddress(), e.getValue()))
            .collect(toList());
   }
}

 

- 위의 코드에서 return문을 살펴보자.

- OrderFlatDto는 컬렉션 필드 대신 컬렉션 필드의 속성으로 구성되어있다.

- findAllByDtoFlat() 메서드에서 OrderFlatDto 객체를 반환하면, 이를 이용하여 후처리 작업을 수행한다.

- 우선 OrderFlatDto의 일부 데이터를 이용하여 OrderQueryDto를 생성한다. 

- 그리고 기존의 컬렉션 필드를 대신했던 컬렉션 필드의 속성 값을 이용하여 OrderItemQueryDto를 생성한다.

- 비어있는 OrderQueryDto의 컬렉션 필드를 OrderItemQueryDto로 채워 넣는다. 

 

- 위의 코드의 핵심은 groupingBy()를 사용한 것이다.

- DB의 입장에서는 중복 데이터가 반환되는 것은 동일하다.

- 하지만, JPA는 groupingBy()를 통해 영속성 Context 상에서 동일한 객체의 값을 하나로 묶는다. 

- 이는 다음과 같은 결과를 JSON으로 반환한다. 

 

[
    {
        "orderId": 11,
        "name": "userB",
        "orderDate": "2022-06-04T16:04:57.769566",
        "orderStatus": "ORDER",
        "address": {
            "city": "Busan",
            "street": "Haeundae",
            "zipcode": "22222"
        },
        "orderItems": [
            {
                "itemName": "Spring1 Book",
                "orderPrice": 20000,
                "count": 3
            },
            {
                "itemName": "Spring2 Book",
                "orderPrice": 40000,
                "count": 4
            }
        ]
    },
    {
        "orderId": 4,
        "name": "userA",
        "orderDate": "2022-06-04T16:04:57.65529",
        "orderStatus": "ORDER",
        "address": {
            "city": "Seoul",
            "street": "Gangnam",
            "zipcode": "11111"
        },
        "orderItems": [
            {
                "itemName": "JPA1 Book",
                "orderPrice": 10000,
                "count": 1
            },
            {
                "itemName": "JPA2 Book",
                "orderPrice": 20000,
                "count": 2
            }
        ]
    }
]

 

- 이처럼 동일한 객체를 하나로 묶어주고, 동일 객체에 한해서 다른 데이터는 배열로 묶어서 표현한다.

 

 

e) groupingBy()의 기준 설정 - @EqualsAndHashCode 

- groupingBy() 메서드는 SQL의 Group By와 동일한 기능을 수행한다.

- 다만, 객체 중점적이라는 차이만 있을 뿐이다. 

 

- 그렇다면 groupingBy()는 어떤 값을 기준으로 객체를 묶는 것일까?

- groupingBy()가 적용되는 대상을 살펴보면 OrderQueryDto 객체인 것을 확인할 수 있다. 

 

 

- 그러므로 groupingBy()의 기준은 OrderQueryDto 클래스에 다음과 같이 설정해준다.

 

@Data
@EqualsAndHashCode(of = "orderId") // v6 메서드의 groupingBy 에서 사용하는 묶음 기준
public class OrderQueryDto {

    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;
    private List<OrderItemQueryDto> orderItems;
    
    // 생성자 생략
}

 

- 위의 코드를 보면 @EqualsAndHashCode라는 어노테이션이 사용되었다.

- 그리고 해당 어노테이션의 매개변수로 "orderId"가 들어있는 것을 확인할 수 있다.

- 이는 OrderQueryDto 객체 간의 비교를 수행할 때, orderId 필드의 값을 기준으로 비교를 수행한다는 의미다.

- 즉, orderId 값이 동일한 객체끼리 groupingBy()를 통해 하나의 객체로 묶이는 것이다. 

 

 

5. DTO 직접 조회 + 한방 쿼리 장단점

a) 장점

- 쿼리 발행 회수가 1회다. 

- 데이터가 많지 않으면 작업 수행 속도가 빠르다. 

 

b) 단점

- 일대다 JOIN으로 인하여 조회 결과에 중복 데이터가 발생된다. 

- 그러므로 Order를 기준으로 페이징이 불가능하다. (= 데이터 뻥튀기, 중복 데이터 발생)

- 애플리케이션 측면세어 보면 후처리 작업이 크다.

 

 

6. 전체 코드 구성

 

- Controller

@RestController
@RequiredArgsConstructor
public class OrderApiController {

    private final OrderQueryRepository orderQueryRepository;

    @GetMapping("/api/v6/orders")
    public List<OrderQueryDto> orderV6 () {
        List<OrderFlatDto> flats = orderQueryRepository.findAllByDtoFlat();

        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());
    }


}

 

- OrderFlatDto

@Data
public class OrderFlatDto {

    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;

    // 컬렉션 필드 대신 컬렉션 필드의 속성으로 구성
    private String itemName;
    private int orderPrice;
    private int count;
    
    // 생성자 생략
}

 

- OrderQueryDto

@Data
@EqualsAndHashCode(of = "orderId") // v6 메서드의 groupingBy 에서 사용하는 묶음 기준
public class OrderQueryDto {

    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;
    private List<OrderItemQueryDto> orderItems;
    
    // 생성자 생략
}

 

- OrderItemQueryDto

@Data
public class OrderItemQueryDto {

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

 

- OrderQueryRepository

@RequiredArgsConstructor
public class OrderQueryRepository {

    private final EntityManager em;

    public List<OrderFlatDto> findAllByDtoFlat() {
        return em.createQuery(
            "select new jpabook.jpabook.dto.OrderFlatDto(o.id, m.name, o.orderDate, o.status, " +
                    "d.adderss, 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();
    }
}

 

 

- 여기까지 DTO 직접 조회 방식으로 컬렉션을 조회하는 방법에 대해서 알아보았다. 

댓글