프로젝트

서점 프로젝트) 카테고리에 해당하는 상품 조회문에서 N + 1 문제 발생.

ruu++ 2024. 6. 26. 21:15

 

QueryDsl에서 상품을 조회 할 때, 상품의 개수만큼 쿼리문이 동작하고 그거에 따라 카테고리 조회문도 상품의 개수만큼 다시 도는 문제가 발생했습니다. 한마디로 20개의 상품을 조회하면 60회의 쿼리문이 동작했습니다.

 

1. 문제의 queryDSL 쿼리문

@Override
public List<Product> findByCategoryIds(Long categoryId) {
    QProduct product = QProduct.product;
    QBookCategory bookCategory = QBookCategory.bookCategory;
    QCategory category = QCategory.category;

    JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);

    // 카테고리 계층 쿼리 (기존 쿼리와 동일)
    BooleanExpression isChildCategory = category.id.eq(categoryId);
    isChildCategory = isChildCategory.or(category.parent.id.eq(categoryId));

    return queryFactory
        .selectFrom(product)
        .leftJoin(product.bookCategories, bookCategory).fetchjoin()
        .leftJoin(bookCategory.category, category).fetchjoin()
        .where(isChildCategory)
        .distinct()
        .fetch();

}

 

fetchJoin될 것을 기대 했지만, 밑의 상황을 보면 fetchJoin으로 동작하지 않는다....

1 - 1. 상황

 

좀 자세하게 살펴보니 product_img_detail 테이블에서 아이디 별로 뭔가 즉시 로딩으로 불러오는 것이 확인 됐습니다.

Product와 ProducImgDetail을 살펴봅시다!!.

 

Product Entity 부분

@OneToOne(mappedBy = "product", fetch = FetchType.LAZY)
@JsonIgnoreProperties("product")
@ToString.Exclude
private ProductImgDetail productImgDetail;

 

ProductImgDetail 부분

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id")
@ToString.Exclude
private Product product;

 

 

2. 여러 해결방법 중 드디어 원인을 찾았습니다.

 

OneToOne Fetch Lazy 적용이 안되는 이유

이전에 영한님 강의에서 본 기억이 어렴풋이 기억났습니다. OneToOne 관계에서 연관관계 주인이 아닌 쪽은 즉시 로딩으로 동작한다는 것이 문제였습니다.

 

 

3. 해결

Product Entity 부분

@OneToOne(mappedBy = "product")
@JsonIgnoreProperties("product")
@ToString.Exclude
private ProductImgDetail productImgDetail;

 

QueryDsl 조회문 부분

@Override
public List<Product> findByCategoryIds(Long categoryId) {
    QProduct product = QProduct.product;
    QProductImgDetail productImgDetail = QProductImgDetail.productImgDetail;
    QBookCategory bookCategory = QBookCategory.bookCategory;
    QCategory category = QCategory.category;

    JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);

    // 카테고리 계층 쿼리 (기존 쿼리와 동일)
    BooleanExpression isChildCategory = category.id.eq(categoryId);
    isChildCategory = isChildCategory.or(category.parent.id.eq(categoryId));
    JPAQuery<Product> productJPAQuery = queryFactory
        .selectFrom(product)
        .leftJoin(product.bookCategories, bookCategory)
        .leftJoin(bookCategory.category, category)
        .leftJoin(product.productImgDetail, productImgDetail).fetchJoin()
        .where(isChildCategory)
        .distinct();
    return productJPAQuery.fetch();
}

 

productImgDetail을 fetchJoin()하는 동작을 추가하여 결과적으로 아래와 같은 쿼리문으로 1번 동작으로 변경 됐습니다. JPA로 개발하면 할수록 이런 쿼리문 최적화가 정말 재밌다고 느껴집니다.

 

쿼리문이 1번으로 최적화 됐다!.