Tech/JPA

연관관계 조회 쿼리를 최적화해보자

주지민 2021. 9. 23. 00:23
반응형
본 포스트는 김영한님의 인프런 강의를 듣고 혼자 공부한 내용입니다

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-API%EA%B0%9C%EB%B0%9C-%EC%84%B1%EB%8A%A5%EC%B5%9C%EC%A0%81%ED%99%94/dashboard

 

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

스프링 부트와 JPA를 활용해서 API를 개발합니다. 그리고 JPA 극한의 성능 최적화 방법을 학습할 수 있습니다., 본 강의는 자바 백엔드 개발의 실전 코스에 있는 활용2 강의 입니다. 스프링 부트와 J

www.inflearn.com


연관관계 ERD

  • relation_order와 relation_user는 1:1(일대일, @OneToOne) 관계
  • relation_order와 relation_delivery는 N:1(다대일, @ManyToOne) 관계
  • relation_order와 relation_item은 N:M 관계이므로 중간에 정션(매핑) 테이블인 relation_order_item을 통해 1:N으로 풀어냈습니다

 

Entity

  • Code
    
    // Entity
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @Getter
    @Table(name = "relation_order")
    @Entity
    public class RelationOrder {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        @OneToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "user_id")
        private RelationUser user;
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "delivery_id")
        private RelationDelivery delivery;
    
        @OneToMany(mappedBy = "order")
        private List<RelationOrderItem> orderItems = new ArrayList<>();
    
        private LocalDateTime registerTime;
        private LocalDateTime updateTime;
        
        ...
    }
  • 모든 관계는 LAZY, 지연로딩으로 설정(@OneToMany는 default가 LAZY)
    • @OneToOne과 @ManyToOne 관계는 default로 FetchType.EAGER가 적용되어있어 조회시 추가적으로 SELECT 쿼리가 발생하게 되고 이는 개발자가 의도하지 않은 N+1 쿼리를 발생시킬 수 도 있기 때문에 LAZY로 전부 바꿔놨습니다

 

최적화 전

  • relation_order를 Id(PK)로 조회하여 Lazy가 설정되어있는 상태인 각 연관관계를 객체 참조를 통해 조회 후 ResponseDTO를 생성
  • Code
    // Convert to responseDto
    public static RelationOrderResponseDto convert(final RelationOrder relationOrder) {
        return new RelationOrderResponseDto(relationOrder.getId(),
                                            RelationUserResponseDto.convert(relationOrder.getUser()),
                                            RelationDeliveryResponseDto.convert(relationOrder.getDelivery()),
                                            convertToRelationItemResponse(relationOrder.getOrderItems()),
                                            relationOrder.getRegisterTime(),
                                            relationOrder.getUpdateTime());
    }
  • 발생 쿼리 분석
    • relation_order 조회 ( JpaRespository.findById )
          
      // findById
      select
          relationor0_.id as id1_8_0_,
          relationor0_.delivery_id as delivery4_8_0_,
          relationor0_.register_time as register2_8_0_,
          relationor0_.update_time as update_t3_8_0_,
          relationor0_.user_id as user_id5_8_0_ 
      from
          relation_order relationor0_ 
      where
          relationor0_.id=?​
    • User 객체 참조 조회시 Lazy 로딩 ( RelationUserResponseDto.convert(relationOrder.getUser()) )
      // user 참조 
      select
          relationus0_.id as id1_10_0_,
          relationus0_.email as email2_10_0_,
          relationus0_.hp as hp3_10_0_,
          relationus0_.name as name4_10_0_,
          relationus0_.register_time as register5_10_0_,
          relationus0_.update_time as update_t6_10_0_ 
      from
          relation_user relationus0_ 
      where
          relationus0_.id=?
    • Delivery 객체 참조 조회시 Lazy 로딩
          
      // Delivery 조회 
      select
          relationde0_.id as id1_6_0_,
          relationde0_.register_time as register2_6_0_,
          relationde0_.type_code as type_cod3_6_0_,
          relationde0_.update_time as update_t4_6_0_ 
      from
          relation_delivery relationde0_ 
      where
          relationde0_.id=?​
    • 연관된 order_item 조회 후 Item 갯수만큼 Select 쿼리 추가..
          
      // item 조회
      select
          relationit0_.id as id1_7_0_,
          relationit0_.name as name2_7_0_,
          relationit0_.price as price3_7_0_,
          relationit0_.register_time as register4_7_0_,
          relationit0_.update_time as update_t5_7_0_ 
      from
          relation_item relationit0_ 
      where
          relationit0_.id=?​

 

최적화 적용

  • 기존 최적화 전 단계에서 사용하는 Lazy 로딩은 개발자가 의도치 않는 N+1 쿼리가 나갈 수 있다는 단점이 있습니다
  • 따라서 조회가 같이 되어야할 연관관계의 경우 FETCH JOIN을 통해 한꺼번에 가져온다면 발생하는 쿼리의 수를 줄일 수 있습니다 ( join 발생 )
  • 추가적으로 컬렉션 조회(@OneToMany) 같은 경우는 FETCH JOIN을 사용할 수 없기 때문에 하이버네이트에서 제공하는 Batch ( 쿼리에 IN절 발생 ) Size를 조절해 최적화 해볼 수 있습니다
    • application.yml
      
      // application.yml
      spring:
        jpa:
          properties:
            hibernate:
              default_batch_fetch_size: 1000
  • JPQL Code
        
    // FETCH JOIN 적용 
    
    @Query("SELECT o FROM RelationOrder o "
            + "JOIN FETCH o.user "
            + "JOIN FETCH o.delivery "
            + "WHERE o.id = :id")
    Optional<RelationOrder> findById(Long id);​
    • 일대일 관계인 User와 FETCH JOIN
    • 다대일 관계인 Delivery와 FETCH JOIN
  • RelationOrder 조회 발생 쿼리
        
    // RelationOrder 조회
    select
        relationor0_.id as id1_8_0_,
        relationus1_.id as id1_10_1_,
        relationde2_.id as id1_6_2_,
        relationor0_.delivery_id as delivery4_8_0_,
        relationor0_.register_time as register2_8_0_,
        relationor0_.update_time as update_t3_8_0_,
        relationor0_.user_id as user_id5_8_0_,
        relationus1_.email as email2_10_1_,
        relationus1_.hp as hp3_10_1_,
        relationus1_.name as name4_10_1_,
        relationus1_.register_time as register5_10_1_,
        relationus1_.update_time as update_t6_10_1_,
        relationde2_.register_time as register2_6_2_,
        relationde2_.type_code as type_cod3_6_2_,
        relationde2_.update_time as update_t4_6_2_ 
    from
        relation_order relationor0_ 
    inner join
        relation_user relationus1_ 
            on relationor0_.user_id=relationus1_.id 
    inner join
        relation_delivery relationde2_ 
            on relationor0_.delivery_id=relationde2_.id 
    where
        relationor0_.id=?

    • FETCH JOIN이 걸린 "relation_user""relation_delivery"에 inner join이 걸린 것을 확인할 수 있다
  • OrderItem ( 정션테이블, 매핑테이블 ) 객체 참조 조회
        
    // OrderItem
    select
        orderitems0_.order_id as order_id3_9_1_,
        orderitems0_.id as id1_9_1_,
        orderitems0_.id as id1_9_0_,
        orderitems0_.item_id as item_id2_9_0_,
        orderitems0_.order_id as order_id3_9_0_ 
    from
        relation_order_item orderitems0_ 
    where
        orderitems0_.order_id=?​
  • RelationItem 조회
        
    // relation_item 조회
    select
        relationit0_.id as id1_7_0_,
        relationit0_.name as name2_7_0_,
        relationit0_.price as price3_7_0_,
        relationit0_.register_time as register4_7_0_,
        relationit0_.update_time as update_t5_7_0_ 
    from
        relation_item relationit0_ 
    where
        relationit0_.id in (
            ?, ?
        )​
    • 기존의 orderItem으로 매핑되어있는 relation_item id마다 SELECT 쿼리가 발생했지만, 하이버네이트 Batch_size를 적용하고 나서 in절로 해당 id들이 묶여 한번의 쿼리로 조회된 것을 확인할 수 있습니다

 

결론

  • 최적화 전
    • 조회 현황
      • relation_order 조회 ( SELECT )
      • LAZY가 적용된 @OneToOne 관계의 relation_user 추가 조회 (SELECT )
      • LAZY가 적용된 @ManyToOne 관계의 relation_delivery 추가 조회 ( SELECT )
      • @OneToMany 관계의 relation_order_item 조회 ( SELECT )
      • relation_order_item과 @ManyToOne 관계인 relation_item 갯수 별로 추가 조회 ( N개의 SELECT )
    • 쿼리 발생 횟수
      • 연관된 아이템이 N개라고 가정했을때 총 4번의 SELECT + N개의 SELECT가 발생합니다 ( 4 + N )
  • 최적화 후
    • 조회 현황
      • relation_order, relation_user, relation_delivery를 FETCH JOIN으로 묶어서 조회 ( SELECT ... INNER JOIN ... )
      • @OneToMany 관계의 relation_order_item 조회 ( SELECT )
      • relation_order_item과 @ManyToOne 관계인 relation_item의 id를 모아 IN절로 한번에 조회 ( SELECT ... IN ... )
    • 쿼리 발생 횟수
      • 연관된 아이템이 1000개(default_batch_size 설정) 미만이라고 가정했을때 총 3번의 SELECT가 발생합니다

 

728x90
반응형