최근에 진행중인 프로젝트에서 JPA N+1 문제에 직면했습니다.
[JPA] JPA N+1 문제 해결 방법 및 성능 비교
프로젝트에서 N+1문제를 해결했던 기존의 포스트가 있습니다
다만, 기존의 프로젝트에서는 쿼리 메소드를 사용하고 @Query 어노테이션을 활용하여 native query에 fetch join을 추가하여 해결했었습니다.
이번에는 Querydsl을 사용한 해결 방법을 작성해보도록 하겠습니다.
먼저, JPA N+1문제가 뭔지 다시 한 번 살펴보겠습니다.
N+1문제
DB에서 객체를 불러올 때 1개의 쿼리가 아니라 연관 관계 객체를 불러오기 위해 N개의 쿼리가 추가로 발생하여 성능이 저하되는 문제입니다.
일대다 연관 관계가 맺어진 객체에서 연관 관계를 참조할 경우 쿼리가 여러 번 발생하는 문제가 발생합니다.
코드를 통해 알아보겠습니다.
Product.class
class Product{
private Long id;
private String name;
private Integer price;
@ManyToOne(fetch = LAZY) // 일대다 연관관계
private Category category;
}
Category.class
class Category{
private Long id;
private String categoryName;
}
ProductService.class
@Transactional(readOnly = true)
public List<ResponseProductDto> getBestProductList(){
return productRepositoryImpl.findBestProducts().stream().map(ResponseProductDto::getAllProductList).collect(Collectors.toList());
}
ProductRepositoryImpl.interface
@Override
public List<Product> findBestProducts(){
List<Product> productList = query
.select(product)
.from(product)
.orderBy(product.count.desc())
.limit(12)
.fetch();
return productList;
}
ResponseProductDto.class
public class ResponseProductDto{
private Long id;
private String name;
private Integer price;
private String categoryName;
public static ResponseProductDto getAllProductList(Product product){
return ResponseProductDto.builder()
.id(product.getId())
.name(product.getName())
.price(product.getPrice())
.categoryName(product.getCategory().getName())
.build();
}
}
Querydsl
에서 findBestProducts()
메소드를 사용하는 경우 @ManyToOne(fetch = LAZY)
때문에 추가 쿼리가 발생하지 않습니다.
하지만, ResponseProductDto::getAllProductList
메소드로 Category 엔티티에 참조하면서 추가 쿼리가 Category 데이터 개수만큼 발생하였습니다.
N+1문제를 해결하기 위해서는 Category 엔티티를 fetch join
을 사용해서 영속화해야 합니다.
Category 엔티티를 fetch join
으로 가져오면 가지는 이점
Fetch Join
이 걸린 연관 엔티티도 함께 select하여 영속화를 할 수 있습니다.Fetch Join
을 통해서 해결할 수 있습니다.
ProductRepositoryImpl.interface
@Override
public List<Product> findBestProducts(){
QProductCategory qProductCategory = QProductCategory.productCategory;
List<Product> productList = query
.select(product)
.from(product)
.leftJoin(product.category, qProductCategory)
.fetchJoin()
.orderBy(product.count.desc())
.limit(12)
.fetch();
return productList;
}
join 식을 써주고 fetchJoin()
을 걸어줍니다.
이렇게 될 경우 Product와 Category 두 개 다 조회가 되며 Category 역시 영속화가 됩니다.
fetch 와 fetchJoin()의 차이
fetch는 결과 반환의 역할을 하고 fetchJoin()은 join한 대상을 영속성 컨텍스트에 저장하는 역할을 합니다.
이렇게 fetch join을 걸어주면 category를 참조할 경우에도 추가 쿼리가 발생하지 않습니다!