Tech/JPA
연관관계 조회 쿼리를 최적화해보자
주지민
2021. 9. 23. 00:23
반응형
본 포스트는 김영한님의 인프런 강의를 듣고 혼자 공부한 내용입니다
연관관계 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=?
- relation_order 조회 ( JpaRespository.findById )
최적화 적용
- 기존 최적화 전 단계에서 사용하는 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
- application.yml
- 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
반응형