[Springboot] Querydsl로 JPA N+1 문제 해결하기

HeavyJ·2023년 6월 25일
0

자바/스프링부트

목록 보기
16/17

최근에 진행중인 프로젝트에서 JPA N+1 문제에 직면했습니다.

[JPA] JPA N+1 문제 해결 방법 및 성능 비교

프로젝트에서 N+1문제를 해결했던 기존의 포스트가 있습니다

다만, 기존의 프로젝트에서는 쿼리 메소드를 사용하고 @Query 어노테이션을 활용하여 native query에 fetch join을 추가하여 해결했었습니다.

이번에는 Querydsl을 사용한 해결 방법을 작성해보도록 하겠습니다.

N+1문제

먼저, JPA N+1문제가 뭔지 다시 한 번 살펴보겠습니다.

N+1문제
DB에서 객체를 불러올 때 1개의 쿼리가 아니라 연관 관계 객체를 불러오기 위해 N개의 쿼리가 추가로 발생하여 성능이 저하되는 문제입니다.

일대다 연관 관계가 맺어진 객체에서 연관 관계를 참조할 경우 쿼리가 여러 번 발생하는 문제가 발생합니다.

N+1문제 발생하는 상황

코드를 통해 알아보겠습니다.

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하여 영속화를 할 수 있습니다.
  • 영속화가 되면 영속성 컨텍스트에 엔티티가 존재하기 때문에 따로 쿼리가 실행되지 않아 N+1 문제가 해결됩니다.

Querydsl로 해결하기

Fetch Join

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를 참조할 경우에도 추가 쿼리가 발생하지 않습니다!

profile
There are no two words in the English language more harmful than “good job”.

0개의 댓글