JPA 지연 로딩에서 연관 관계가 설정된 엔티티를 조회할 경우(1)에 조회된 데이터 개수(n) 만큼 연관관계의 조회 쿼리가 추가로 발생하는 현상
해당 문제의 해결을 위해 JPQL의 fetch join과 Entity Graph를 사용하였다. 다만 패치 조인의 경우 1:n 관계에서 두 번 이상 사용하면 Cartesian Product 문제가 발생할 수 있다. 이는 두 테이블 간의 조인 시 발생하는 문제로 조인된 결과가 원하는 것보다 훨씬 크게 나타날 수 있으며 중복된 레코드를 생성하여 DB에 부담을 주거나 성능 문제를 야기할 수 있다. 따라서 두 번 이상의 패치 조인이 필요한 경우에는 엔티티 그래프를 사용하였다.
우리 프로젝트에서는 cart의 이름을 memberProduct로 설정하였다. cart가 memeber와 product 엔티티의 n:n 관계에서 중간 테이블 역할을 하기 때문이다. 즉 memberProduct(cart) 엔티티는 product, member 엔티티와 각각 1:n 관계를 가진다. memberProduct repository에서 n+1 문제 해결을 위한 쿼리 튜닝을 진행하였다.
public List<MemberProductResponseDto> getCart(Member member) {
List<MemberProduct> memberProductList = memberProductRepository.findByMemberId(
member.getId());
return memberProductList.stream()
.map(MemberProductMapper.INSTANCE::toResponseDto)
.toList();
}
// MemberProduct Repository
List<MemberProduct> findByMemberId(Long memberId);
@Query("SELECT mp FROM MemberProduct mp JOIN FETCH mp.product p JOIN FETCH p.productImageList WHERE mp.member.id = :memberId")
List<MemberProduct> findByMemberId(@Param("memberId") Long memberId);
fetch join 후 1+n 문제 해결과 실행 시간 감소
다른 로직과 비교
다만 해당 로직에서 member - product(1:n), product - product image(1:n) 관계로 1:n 관계를 이중으로 갖게 되어 패치 조인도 두 번 사용된 것을 확인할 수 있다. 이러한 경우에는 패치 조인보다 엔티티 그래프를 사용하는 것이 DB에 덜 부담될 것이라 생각했으므로 엔티티 그래프로 변경하였다.
// MemberProduct Entity
@NamedEntityGraph(
name = "graph.MemberProduct",
attributeNodes = {
@NamedAttributeNode(value = "product", subgraph = "productGraph")
}, subgraphs = {
@NamedSubgraph(name = "productGraph", attributeNodes = @NamedAttributeNode("productImageList"))
})
public class MemberProduct {}
// MemberProduct Repository
@EntityGraph(value = "graph.MemberProduct", type = EntityGraph.EntityGraphType.FETCH)
List<MemberProduct> findByMemberId(Long memberId);
@Modifying
@Query("DELETE FROM MemberProduct mp WHERE mp.member.id = :memberId")
void deleteByMemberId(@Param("memberId") Long memberId);
// purchase entity
@NamedEntityGraph(name = "graph.Purchase",
attributeNodes = {
@NamedAttributeNode(value = "purchaseProductList", subgraph = "purchaseProductListGraph")
})
public class Purchase {}
// purchase repository
@EntityGraph(value = "graph.Purchase", type = EntityGraph.EntityGraphType.FETCH)
@Query("SELECT p FROM Purchase p WHERE p.member.id = :memberId ORDER BY p.createdAt DESC")
List<Purchase> findByMemberId(@Param("memberId") Long memberId);