[JPA] 컬렉션 조회 API 최적화

3Beom's 개발 블로그·2023년 7월 2일
0

SpringJPA

목록 보기
19/21
post-custom-banner

출처

본 글은 인프런의 김영한님 강의 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 을 수강하며 기록한 필기 내용을 정리한 글입니다.

-> 인프런
-> 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 강의


  • Order : OrderItem = 1 : N, OrderItem : Item = N : 1 관계일 때, Order 엔티티에서 OrderItem과 Item을 조회한다.
  • 즉, Order 엔티티에서 컬렉션으로 갖고 있는 OrderItem을 조회하고, 해당 OrderItem이 어느 Item에 해당하는지 정보를 조회하는 것.
  • @~~ToMany 어노테이션이 적용되어 있는 컬렉션 필드 조회 최적화

주문 조회 V1 : 엔티티 직접 노출

쓰지말자

주문 조회 V2 : 엔티티를 DTO로 변환

  • 다음과 같이 모든 엔티티에 대한 DTO들을 생성하고, 엔티티를 직접 반환하는 것이 아닌 DTO를 반환하도록 설정한다.
  • DTO 내에도 엔티티가 포함되면 안된다.
    • OrderResponseDto 내 OrderItem도 엔티티가 아닌 DTO로 반환해야 한다.
    • OrderItemResponseDto 내 Item도 ItemResponseDto로 반환해야 한다.
	@GetMapping("/order/v2")
    public OrderResponseFormat<List<OrderResponseDto>> ordersV2() {
        return new OrderResponseFormat<>("주문 상세 조회 완료",
                orderService.findOrders(new OrderSearch())
                        .stream()
                        .map(OrderResponseDto::new)
                        .collect(Collectors.toList()));
    }

    @Data
    @AllArgsConstructor
    static class OrderResponseFormat<T> {
        String message;
        T Data;
    }

    @Data
    static class OrderResponseDto {
        private Long orderId;
        private MemberResponseDto member;
        private List<OrderItemResponseDto> orderItems = new ArrayList<>();
        private DeliveryResponseDto delivery;
        private LocalDateTime orderDate;
        private OrderStatus status;

        public OrderResponseDto(Order order) {
            this.orderId = order.getId();
            this.member = new MemberResponseDto(order.getMember());
            this.orderItems = order.getOrderItems()
                    .stream()
                    .map(OrderItemResponseDto::new)
                    .collect(Collectors.toList());
            this.delivery = new DeliveryResponseDto(order.getDelivery());
            this.orderDate = order.getOrderDate();
            this.status = order.getStatus();
        }
    }

    @Data
    static class MemberResponseDto {
        private Long memberId;
        private String name;

        public MemberResponseDto(Member member) {
            this.memberId = member.getId();
            this.name = member.getName();
        }
    }

    @Data
    static class DeliveryResponseDto {
        private Long deliveryId;
        private Address address;
        private DeliveryStatus status;

        public DeliveryResponseDto(Delivery delivery) {
            this.deliveryId = delivery.getId();
            this.address = delivery.getAddress();
            this.status = delivery.getStatus();
        }
    }

    @Data
    static class OrderItemResponseDto {
        private Long orderItemId;
        private ItemResponseDto itemInfo;
        private Integer orderPrice;
        private Integer count;

        public OrderItemResponseDto(OrderItem orderItem) {
            this.orderItemId = orderItem.getId();
            this.itemInfo = new ItemResponseDto(orderItem.getItem());
            this.orderPrice = orderItem.getOrderPrice();
            this.count = orderItem.getCount();
        }
    }

    @Data
    static class ItemResponseDto {
        private Long itemId;
        private String name;

        public ItemResponseDto(Item item) {
            this.itemId = item.getId();
            this.name = item.getName();
        }
    }
  • 추후에 각 DTO들은 각 domain 내 dto 패키지에 모아두는 것이 좋다.
  • 이렇게만 활용할 경우, DB로 쿼리가 매우 많이 나간다.
    • Order 조회 쿼리
    • Order에 대한 Member, Delivery 를 조회하는 쿼리 (Order 조회 결과만큼)
    • Order에 대한 각 OrderItem를 조회하는 쿼리 (Order 조회 결과만큼)
    • OrderItem에 대한 각 Item을 조회하는 쿼리 (OrderItem 조회 결과만큼)
  • 컬렉션 페치 조인을 활용해야 한다.

주문 조회 V3 : 페치 조인 최적화

  • 만약 다음과 같이 단순하게 페치 조인을 짤 경우, Order 데이터 조회 결과가 뻥튀기가 되어버린다.
    	@Override
        public List<Order> findAllWithItem(OrderSearch orderSearch) {
            return em.createQuery(
                    "SELECT o FROM Order o "
                            + "JOIN FETCH o.member "
                            + "JOIN FETCH o.delivery d "
                            + "JOIN FETCH o.orderItems oi "
                            + "JOIN FETCH oi.item i", Order.class)
                    .getResultList();
        }
    • Order : OrderItem = 1 : N 이기 때문에, 각 Order 데이터마다 N개(여러개)의 OrderItem이 있기 때문에 각 OrderItem 데이터에 맞춰서 Order 데이터가 추가되어 버린다.
    • DB 조회 결과를 생각해보면, N 측에 해당하는 데이터만 서로 다르고, 1측에 해당하는 데이터는 그만큼 중복되어 출력된다.

  • 따라서 다음과 같이 응답이 출력되어 버린다.

    {
        "message": "주문 상세 조회 완료",
        "data": [
            {
                "orderId": 4,
                "member": {
                    "memberId": 1,
                    "name": "userA"
                },
                "orderItems": [
                    {
                        "orderItemId": 6,
                        "itemInfo": {
                            "itemId": 2,
                            "name": "JPA1 BOOK"
                        },
                        "orderPrice": 10000,
                        "count": 1
                    },
                    {
                        "orderItemId": 7,
                        "itemInfo": {
                            "itemId": 3,
                            "name": "JPA2 BOOK"
                        },
                        "orderPrice": 20000,
                        "count": 2
                    }
                ],
                "delivery": {
                    "deliveryId": 5,
                    "address": {
                        "city": "서울",
                        "street": "1",
                        "zipcode": "1111"
                    },
                    "status": null
                },
                "orderDate": "2023-07-03T03:11:06.601545",
                "status": "ORDER"
            },
            {
                "orderId": 4,
                "member": {
                    "memberId": 1,
                    "name": "userA"
                },
                "orderItems": [
                    {
                        "orderItemId": 6,
                        "itemInfo": {
                            "itemId": 2,
                            "name": "JPA1 BOOK"
                        },
                        "orderPrice": 10000,
                        "count": 1
                    },
                    {
                        "orderItemId": 7,
                        "itemInfo": {
                            "itemId": 3,
                            "name": "JPA2 BOOK"
                        },
                        "orderPrice": 20000,
                        "count": 2
                    }
                ],
                "delivery": {
                    "deliveryId": 5,
                    "address": {
                        "city": "서울",
                        "street": "1",
                        "zipcode": "1111"
                    },
                    "status": null
                },
                "orderDate": "2023-07-03T03:11:06.601545",
                "status": "ORDER"
            },
            {
                "orderId": 11,
                "member": {
                    "memberId": 8,
                    "name": "userB"
                },
                "orderItems": [
                    {
                        "orderItemId": 13,
                        "itemInfo": {
                            "itemId": 9,
                            "name": "SPRING1 BOOK"
                        },
                        "orderPrice": 20000,
                        "count": 3
                    },
                    {
                        "orderItemId": 14,
                        "itemInfo": {
                            "itemId": 10,
                            "name": "SPRING2 BOOK"
                        },
                        "orderPrice": 40000,
                        "count": 4
                    }
                ],
                "delivery": {
                    "deliveryId": 12,
                    "address": {
                        "city": "진주",
                        "street": "2",
                        "zipcode": "2222"
                    },
                    "status": null
                },
                "orderDate": "2023-07-03T03:11:06.634574",
                "status": "ORDER"
            },
            {
                "orderId": 11,
                "member": {
                    "memberId": 8,
                    "name": "userB"
                },
                "orderItems": [
                    {
                        "orderItemId": 13,
                        "itemInfo": {
                            "itemId": 9,
                            "name": "SPRING1 BOOK"
                        },
                        "orderPrice": 20000,
                        "count": 3
                    },
                    {
                        "orderItemId": 14,
                        "itemInfo": {
                            "itemId": 10,
                            "name": "SPRING2 BOOK"
                        },
                        "orderPrice": 40000,
                        "count": 4
                    }
                ],
                "delivery": {
                    "deliveryId": 12,
                    "address": {
                        "city": "진주",
                        "street": "2",
                        "zipcode": "2222"
                    },
                    "status": null
                },
                "orderDate": "2023-07-03T03:11:06.634574",
                "status": "ORDER"
            }
        ]
    }
    • 똑같은 데이터가 중복되어 응답되는 것을 확인할 수 있다.
  • 이 때 JPQL에 DISTINCT 키워드를 붙이면 중복되는 데이터를 줄일 수 있다.

    • OrderRepositoryImpl

      	@Override
          public List<Order> findAllWithItem(OrderSearch orderSearch) {
              return em.createQuery(
                      "SELECT DISTINCT o FROM Order o "
                              + "JOIN FETCH o.member "
                              + "JOIN FETCH o.delivery d "
                              + "JOIN FETCH o.orderItems oi "
                              + "JOIN FETCH oi.item i", Order.class)
                      .getResultList();
          }
    • 응답 결과

      {
          "message": "주문 상세 조회 완료",
          "data": [
              {
                  "orderId": 4,
                  "member": {
                      "memberId": 1,
                      "name": "userA"
                  },
                  "orderItems": [
                      {
                          "orderItemId": 6,
                          "itemInfo": {
                              "itemId": 2,
                              "name": "JPA1 BOOK"
                          },
                          "orderPrice": 10000,
                          "count": 1
                      },
                      {
                          "orderItemId": 7,
                          "itemInfo": {
                              "itemId": 3,
                              "name": "JPA2 BOOK"
                          },
                          "orderPrice": 20000,
                          "count": 2
                      }
                  ],
                  "delivery": {
                      "deliveryId": 5,
                      "address": {
                          "city": "서울",
                          "street": "1",
                          "zipcode": "1111"
                      },
                      "status": null
                  },
                  "orderDate": "2023-07-03T03:12:57.728468",
                  "status": "ORDER"
              },
              {
                  "orderId": 11,
                  "member": {
                      "memberId": 8,
                      "name": "userB"
                  },
                  "orderItems": [
                      {
                          "orderItemId": 13,
                          "itemInfo": {
                              "itemId": 9,
                              "name": "SPRING1 BOOK"
                          },
                          "orderPrice": 20000,
                          "count": 3
                      },
                      {
                          "orderItemId": 14,
                          "itemInfo": {
                              "itemId": 10,
                              "name": "SPRING2 BOOK"
                          },
                          "orderPrice": 40000,
                          "count": 4
                      }
                  ],
                  "delivery": {
                      "deliveryId": 12,
                      "address": {
                          "city": "진주",
                          "street": "2",
                          "zipcode": "2222"
                      },
                      "status": null
                  },
                  "orderDate": "2023-07-03T03:12:57.761936",
                  "status": "ORDER"
              }
          ]
      }
    • 다음과 같이 데이터 중복이 제거된다.

    • 해당 기능은 DB에 쿼리를 보낼 때 distinct 키워드를 붙이기도 하지만, JPA 자체적으로 중복을 제거하는 기능도 제공해준다.

    • 즉, 만약 DB에서 join한 결과가 완전히 똑같은 row는 중복이 제거되고(원래 DB의 distinct 기능), 이와 더불어 조회된 메인 엔티티(Order) 데이터 중 똑같은 식별자(pk)를 가진 중복 데이터들은 제거해서 반환해준다.

    • DB에서 중복 제거로 조회 해오고, 애플리케이션 단에서 한번 더 중복을 제거하는 것.

  • 이렇게 구현하면 쿼리가 한번만 나가는 것을 볼 수 있다.

    	select
            distinct order0_.order_id as order_id1_9_0_,
            member1_.member_id as member_i1_6_1_,
            delivery2_.delivery_id as delivery1_3_2_,
            orderitems3_.order_item_id as order_it1_8_3_,
            item4_.item_id as item_id2_4_4_,
            order0_.created_at as created_2_9_0_,
            order0_.modified_at as modified3_9_0_,
            order0_.delivery_id as delivery6_9_0_,
            order0_.member_id as member_i7_9_0_,
            order0_.orderdate as orderdat4_9_0_,
            order0_.status as status5_9_0_,
            member1_.created_at as created_2_6_1_,
            member1_.modified_at as modified3_6_1_,
            member1_.city as city4_6_1_,
            member1_.street as street5_6_1_,
            member1_.zipcode as zipcode6_6_1_,
            member1_.name as name7_6_1_,
            delivery2_.created_at as created_2_3_2_,
            delivery2_.modified_at as modified3_3_2_,
            delivery2_.city as city4_3_2_,
            delivery2_.street as street5_3_2_,
            delivery2_.zipcode as zipcode6_3_2_,
            delivery2_.status as status7_3_2_,
            orderitems3_.created_at as created_2_8_3_,
            orderitems3_.modified_at as modified3_8_3_,
            orderitems3_.count as count4_8_3_,
            orderitems3_.item_id as item_id6_8_3_,
            orderitems3_.order_id as order_id7_8_3_,
            orderitems3_.orderprice as orderpri5_8_3_,
            orderitems3_.order_id as order_id7_8_0__,
            orderitems3_.order_item_id as order_it1_8_0__,
            item4_.created_at as created_3_4_4_,
            item4_.modified_at as modified4_4_4_,
            item4_.name as name5_4_4_,
            item4_.price as price6_4_4_,
            item4_.stockquantity as stockqua7_4_4_,
            item4_1_.artist as artist1_0_4_,
            item4_1_.etc as etc2_0_4_,
            item4_2_.author as author1_1_4_,
            item4_2_.isbn as isbn2_1_4_,
            item4_3_.actor as actor1_7_4_,
            item4_3_.director as director2_7_4_,
            item4_.dtype as dtype1_4_4_ 
        from
            orders order0_ 
        inner join
            member member1_ 
                on order0_.member_id=member1_.member_id 
        inner join
            delivery delivery2_ 
                on order0_.delivery_id=delivery2_.delivery_id 
        inner join
            order_item orderitems3_ 
                on order0_.order_id=orderitems3_.order_id 
        inner join
            item item4_ 
                on orderitems3_.item_id=item4_.item_id 
        left outer join
            album item4_1_ 
                on item4_.item_id=item4_1_.item_id 
        left outer join
            book item4_2_ 
                on item4_.item_id=item4_2_.item_id 
        left outer join
            movie item4_3_ 
                on item4_.item_id=item4_3_.item_id
  • ⭐ 하지만 컬렉션을 페치 조인 할 경우, 페이징이 불가능해진다.

  • 위 코드에서 다음과 같이 페이징을 적용할 경우,

    	@Override
        public List<Order> findAllWithItem(OrderSearch orderSearch) {
            return em.createQuery(
                    "SELECT DISTINCT o FROM Order o "
                            + "JOIN FETCH o.member "
                            + "JOIN FETCH o.delivery d "
                            + "JOIN FETCH o.orderItems oi "
                            + "JOIN FETCH oi.item i", Order.class)
                    .setFirstResult(1)
                    .setMaxResults(100)
                    .getResultList();
        }
    • 다음과 같이 결과는 잘 나온다.

      {
          "message": "주문 상세 조회 완료",
          "data": [
              {
                  "orderId": 11,
                  "member": {
                      "memberId": 8,
                      "name": "userB"
                  },
                  "orderItems": [
                      {
                          "orderItemId": 13,
                          "itemInfo": {
                              "itemId": 9,
                              "name": "SPRING1 BOOK"
                          },
                          "orderPrice": 20000,
                          "count": 3
                      },
                      {
                          "orderItemId": 14,
                          "itemInfo": {
                              "itemId": 10,
                              "name": "SPRING2 BOOK"
                          },
                          "orderPrice": 40000,
                          "count": 4
                      }
                  ],
                  "delivery": {
                      "deliveryId": 12,
                      "address": {
                          "city": "진주",
                          "street": "2",
                          "zipcode": "2222"
                      },
                      "status": null
                  },
                  "orderDate": "2023-07-03T03:21:54.991072",
                  "status": "ORDER"
              }
          ]
      }
    • 하지만 쿼리를 자세히 보면,

      		select
              distinct order0_.order_id as order_id1_9_0_,
              member1_.member_id as member_i1_6_1_,
              delivery2_.delivery_id as delivery1_3_2_,
              orderitems3_.order_item_id as order_it1_8_3_,
              item4_.item_id as item_id2_4_4_,
              order0_.created_at as created_2_9_0_,
              order0_.modified_at as modified3_9_0_,
              order0_.delivery_id as delivery6_9_0_,
              order0_.member_id as member_i7_9_0_,
              order0_.orderdate as orderdat4_9_0_,
              order0_.status as status5_9_0_,
              member1_.created_at as created_2_6_1_,
              member1_.modified_at as modified3_6_1_,
              member1_.city as city4_6_1_,
              member1_.street as street5_6_1_,
              member1_.zipcode as zipcode6_6_1_,
              member1_.name as name7_6_1_,
              delivery2_.created_at as created_2_3_2_,
              delivery2_.modified_at as modified3_3_2_,
              delivery2_.city as city4_3_2_,
              delivery2_.street as street5_3_2_,
              delivery2_.zipcode as zipcode6_3_2_,
              delivery2_.status as status7_3_2_,
              orderitems3_.created_at as created_2_8_3_,
              orderitems3_.modified_at as modified3_8_3_,
              orderitems3_.count as count4_8_3_,
              orderitems3_.item_id as item_id6_8_3_,
              orderitems3_.order_id as order_id7_8_3_,
              orderitems3_.orderprice as orderpri5_8_3_,
              orderitems3_.order_id as order_id7_8_0__,
              orderitems3_.order_item_id as order_it1_8_0__,
              item4_.created_at as created_3_4_4_,
              item4_.modified_at as modified4_4_4_,
              item4_.name as name5_4_4_,
              item4_.price as price6_4_4_,
              item4_.stockquantity as stockqua7_4_4_,
              item4_1_.artist as artist1_0_4_,
              item4_1_.etc as etc2_0_4_,
              item4_2_.author as author1_1_4_,
              item4_2_.isbn as isbn2_1_4_,
              item4_3_.actor as actor1_7_4_,
              item4_3_.director as director2_7_4_,
              item4_.dtype as dtype1_4_4_ 
          from
              orders order0_ 
          inner join
              member member1_ 
                  on order0_.member_id=member1_.member_id 
          inner join
              delivery delivery2_ 
                  on order0_.delivery_id=delivery2_.delivery_id 
          inner join
              order_item orderitems3_ 
                  on order0_.order_id=orderitems3_.order_id 
          inner join
              item item4_ 
                  on orderitems3_.item_id=item4_.item_id 
          left outer join
              album item4_1_ 
                  on item4_.item_id=item4_1_.item_id 
          left outer join
              book item4_2_ 
                  on item4_.item_id=item4_2_.item_id 
          left outer join
              movie item4_3_ 
                  on item4_.item_id=item4_3_.item_id
    • 마지막에 limit 옵션이 없는 것을 확인할 수 있다.

    • 또한, 다음과 같이 WARN 로그가 찍힌다.

    • 즉, DB에서는 결과 그대로 가져오고, 해당 결과를 메모리에 올려놓고 애플리케이션 단에서 내부적으로 페이징 로직이 이루어지게 된다.

    • 이렇게 되면 DB 결과가 매우 클 경우, 메모리가 초과되어 버리거나 성능적으로 매우 부정적인 영향을 끼칠 것이다.

    • 또한 뻥튀기 되어버린 결과를 기준으로 페이징이 일어나게 된다.

  • ⭐ 따라서 일대다 컬렉션 페치 조인에서는 페이징 기능을 쓰면 안된다.

    • ?대일 연관관계(@~~ToOne) 페치 조인은 페이징 가능하다!
    • ?대다 연관관계(@~~ToMany) 컬렉션 페치 조인이 페이징 불가능하다.

⭐ 주문 조회 V3.1 : 컬렉션 페치 조인 페이징 한계 돌파 ⭐

  • 컬렉션 페치 조인의 경우, 페이징이 불가능하다.
    • 컬렉션을 페치 조인 할 경우, Join이 발생하므로 데이터가 예측할 수 없이 증가하게 된다.
    • 일대다(1:N) 관계에서 일(1)측을 기준으로 페이징 해야 하는데, 데이터는 다(N)측을 기준으로 생성된다.
    • 이에 따라 다 측을 기준으로 페이징이 되어버리게 되고, 그러면 일 측의 데이터는 중복된 상태로 조회되는데, 우리가 원하던 결과가 안나올 것이다.
  • 이러한 경우, 다음의 방식으로 해결할 수 있다.
    1. 먼저 페이징 기준으로 두고 싶은 엔티티에서 필요한 @~~ToOne 엔티티를 페치조인하여 결과를 가져온다.
      • 위 예시의 경우, Order 엔티티에서 @~~ToOne 관계인 Member와 Delivery를 페치조인해서 결과를 가져온다.
      • @~~ToOne 관계만 페치 조인하는 경우는 페이징 처리가 가능하다.
    2. 그 다음 컬렉션은 지연 로딩으로 조회한다.
      • Order 엔티티에서 @~~ToMany 관계인 OrderItem은 getOrderItem().getId() 와 같이 지연 로딩으로 가져온다.
    3. 만약 아무 설정도 하지 않으면 N + 1 문제가 발생하지만, 다음과 같이 batch size 설정을 한다.
      • 글로벌 설정 : hibernate.default_batch_fetch_size
        • application.yml

          spring:
            datasource:
              url: jdbc:h2:tcp://localhost/~/jpashop
              username: sa
              password:
              driver-class-name: org.h2.Driver
            jpa:
              hibernate:
                ddl-auto: create
              properties:
                hibernate:
            #      show_sql: true
                  format_sql: true
                  default_batch_fetch_size: 100
          
          logging.level:
            org.hibernate.SQL: debug
            org.hibernate.type: trace #스프링 부트 2.x, hibernate5
          # org.hibernate.orm.jdbc.bind: trace #스프링 부트 3.x, hibernate6
      • 필드별 설정 : @BatchSize
        • Order

          ...
          
          	@BatchSize(size = 500)
              @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
              private List<OrderItem> orderItems = new ArrayList<>();
          
          ...
      • 해당 옵션을 사용할 경우, 컬렉션이나 프록시 객체를 한꺼번에 설정한 batch size 만큼 IN 쿼리로 조회하게 된다.
      • 즉, 원래는 다음과 같이 동작하는 것을
        • Order 조회 → N개 조회됨
        • 각 N개에 해당하는 OrderItem 각각 조회 쿼리 전송 → M개 조회됨
        • 각 M개에 해당하는 Item 각각 조회 쿼리 전송
      • 다음과 같이 동작하도록 바꾼다.
        • Order 조회 → N개 조회됨
        • 각 N개에 해당하는 OrderItem 조회할 때, where절에 다음과 같이 IN 조건을 적용해서 한번에 가져옴
          • ... WHERE order_id in (?, ?, ?, ...)
          • M개 조회됨
        • 똑같이 M개에 해당하는 Item 조회할 때, where절에 IN 조건을 적용해서 한번에 가져옴
          • ... WHERE item_id in (?, ?, ?, ...)
      • 즉, batch size를 설정한 것은 WHERE 절에 IN 조건 적용할 때의 최대 개수를 설정하는 것이다.
      • 이와 같이 설정하면 다음과 같이 쿼리가 전송된다.
        • 처음 Order 조회하는 쿼리 : @~~ToOne 관계만 페치조인함
          	select
                  order0_.order_id as order_id1_9_0_,
                  member1_.member_id as member_i1_6_1_,
                  delivery2_.delivery_id as delivery1_3_2_,
                  order0_.created_at as created_2_9_0_,
                  order0_.modified_at as modified3_9_0_,
                  order0_.delivery_id as delivery6_9_0_,
                  order0_.member_id as member_i7_9_0_,
                  order0_.orderdate as orderdat4_9_0_,
                  order0_.status as status5_9_0_,
                  member1_.created_at as created_2_6_1_,
                  member1_.modified_at as modified3_6_1_,
                  member1_.city as city4_6_1_,
                  member1_.street as street5_6_1_,
                  member1_.zipcode as zipcode6_6_1_,
                  member1_.name as name7_6_1_,
                  delivery2_.created_at as created_2_3_2_,
                  delivery2_.modified_at as modified3_3_2_,
                  delivery2_.city as city4_3_2_,
                  delivery2_.street as street5_3_2_,
                  delivery2_.zipcode as zipcode6_3_2_,
                  delivery2_.status as status7_3_2_ 
              from
                  orders order0_ 
              inner join
                  member member1_ 
                      on order0_.member_id=member1_.member_id 
              inner join
                  delivery delivery2_ 
                      on order0_.delivery_id=delivery2_.delivery_id limit ?
          • 페이징 처리도 되는 것을 확인할 수 있음. : limit ?
        • Order 조회 결과에 대한 OrderItem 지연 로딩 : WHERE + IN 활용됨
          	select
                  orderitems0_.order_id as order_id7_8_1_,
                  orderitems0_.order_item_id as order_it1_8_1_,
                  orderitems0_.order_item_id as order_it1_8_0_,
                  orderitems0_.created_at as created_2_8_0_,
                  orderitems0_.modified_at as modified3_8_0_,
                  orderitems0_.count as count4_8_0_,
                  orderitems0_.item_id as item_id6_8_0_,
                  orderitems0_.order_id as order_id7_8_0_,
                  orderitems0_.orderprice as orderpri5_8_0_ 
              from
                  order_item orderitems0_ 
              where
                  orderitems0_.order_id in (
                      ?, ?
                  )
        • OrderItem 조회 결과에 대한 Item 지연 로딩 : WHERE + IN 활용됨
          	select
                  item0_.item_id as item_id2_4_0_,
                  item0_.created_at as created_3_4_0_,
                  item0_.modified_at as modified4_4_0_,
                  item0_.name as name5_4_0_,
                  item0_.price as price6_4_0_,
                  item0_.stockquantity as stockqua7_4_0_,
                  item0_1_.artist as artist1_0_0_,
                  item0_1_.etc as etc2_0_0_,
                  item0_2_.author as author1_1_0_,
                  item0_2_.isbn as isbn2_1_0_,
                  item0_3_.actor as actor1_7_0_,
                  item0_3_.director as director2_7_0_,
                  item0_.dtype as dtype1_4_0_ 
              from
                  item item0_ 
              left outer join
                  album item0_1_ 
                      on item0_.item_id=item0_1_.item_id 
              left outer join
                  book item0_2_ 
                      on item0_.item_id=item0_2_.item_id 
              left outer join
                  movie item0_3_ 
                      on item0_.item_id=item0_3_.item_id 
              where
                  item0_.item_id in (
                      ?, ?, ?, ?
                  )
  • 즉, 위와 같이 설정할 경우, 다음과 같이 동작하게 된다.
    • Order 엔티티와, @~~ToOne 연관관계인 정보들 페치 조인으로 한번에 다 가져옴
    • @~~ToMany 연관관계인 컬렉션 정보들은 지연 로딩으로 가져옴
    • 가져올 때 batch size 설정으로 인해 WHERE 절에 IN 조건이 부여되어 각각 따로 가져오는게 아니라 한번에 가져오게 됨.
  • 따라서 1 - N - N 으로 늘어나던 쿼리 양이 1 - 1 - 1 로 줄여버린다.

V3 vs V3.1

  • V3
    • 페이징 불가능
    • 한번의 쿼리로 데이터 모두 가져올 수 있음
    • 조회되는 데이터에 중복이 많다.
  • V3.1
    • 페이징 가능
    • 한번은 아니지만 적은 쿼리로 데이터 모두 가져올 수 있음
    • 조회되는 데이터에 중복이 적다.
  • 페이징이 필요 없는 경우, V3는 한번의 쿼리만으로 모든 데이터를 가져올 수 있지만, 해당 방식이 항상 유리한건 아닐 수 있다.
    • V3
      • V3의 경우, 필요한 모든 테이블들을 join해서 그 결과를 DB로부터 가져오고, 애플리케이션 단에서 다시 한번 중복을 제거하게 된다.
      • 이 경우, 만약 N측(컬렉션)의 데이터 양이 매우 많을 경우, 데이터 전송량 자체가 많아지게 된다.
      • DB에서 1:N 관계 조인하는 결과를 보면, 데이터 양은 N 측에 맞춰지게 되고, 이에 따라 1 측에 해당하는 데이터는 중복이 발생할 수 밖에 없다.
      • 결국, 엄청난 데이터 중복이 발생한 결과가 조회되어 DB → 서버 로 전송되게 되는 것.
    • V3.1
      • 반면 V3.1의 경우, DB로 쿼리는 여러번 전송되지만, 각 쿼리마다 딱 정해진, 필요한 데이터들만 조회되게 된다. : 중복이 최소화된다.
      • 위 예시의 경우에도 Order 엔티티 채울 때 딱 정해진 만큼만, 이후 OrderItems 채울 때도 IN 절로 딱 정해진 만큼만, 이후 Item 채울 때도 IN 절로 딱 정해진 만큼만 조회하게 된다.
      • 즉, V3에 비해 정규화 된, 중복이 최소화 된 결과가 DB로부터 전송된다.

→ 만약 테이블 join 결과에 1측 테이블 중복 데이터가 너무 많을 경우, 페이징이 필요 없더라도 V3.1 방식이 더 좋을 수 있다.

결론

  • @~~ToOne 관계는 페치 조인 해도 페이징에 영향을 주지 않는다.
  • 따라서 @~~ToOne 관계는 페치 조인으로 쿼리 수를 줄이고, 나머지는 hibernate.default_batch_fetch_size 로 최적화하자.
  • batch size는 최소 값은 없는데 최대 값은 있다. : 1000개
    • IN 조건에 포함되는 개수가 1000개를 넘어가면 오류를 일으키는 DB들이 있기 때문이다.
    • DB에 따라 IN 절 파라미터를 1000으로 제한하기도 함.
    • 1000으로 잡게 되면 한번에 1000개를 DB에서 애플리케이션으로 불러오게 되므로 DB에 순간 부하가 증가할 수 있다.
  • 보통은 100~1000 사이를 활용한다.
    • 보통은 클 수록 성능이 개선된다.
    • 하지만 순간적인 부하를 얼마나 감당할 수 있느냐를 고려해야함.
  • 애플리케이션은 100이든 1000이든 어차피 전체 데이터를 로딩 해야하기 때문에 메모리 사용량은 같다.
  • 1000으로 설정하는 것이 성능 상 가장 좋지만, 결국 DB든 애플리케이션이든 순간 부하를 어디까지 견딜 수 있는지에 따라 결정하면 된다.
profile
경험과 기록으로 성장하기
post-custom-banner

0개의 댓글