JPA Repository를 활용해 인터페이스 메소드를 호출 할 때 실행하는 첫 쿼리에서 하위 엔티티까지 한 번에 가져오지 않고, 하위 엔티티를 프록시 클래스로 받아오게 된다.
프록시 클래스로 내려받은 하위 엔티티는 데이터를 사용할 때 초기화를 하는데, ID값으로 조회를 하기 때문에 반복문을 사용할 경우 하위 엔티티의 갯수 만큼 쿼리문이 발생하게 된다
@Transactional
public List<BasketResponseDto> inquiryBasket(Long userId) {
Member member = memberRepository.findById(userId).orElseThrow(
() -> new CustomException(BAD_REQUEST)
);
// member 객체의 ID값을 이용해서 basket 객체를 조회하는 쿼리 발생
// basket 객체 안에는 Products에 대한 데이터가 담겨있지 않고 프록시 객체로 영속성 컨텍스트에 저장된다.
List<Basket> baskets = basketRepository.findByMember(member);
List<BasketResponseDto> basketResponseDtoList = new ArrayList<>();
for (Basket basket : baskets) {
// basket 객체에서 Products에 대한 데이터를 조회하기 때문에 프록시 객체를 초기화하기 위한 쿼리가 발생
// 문제는 basket 객체와 연관된 Products의 ID값을 조건으로 조회하기 때문에 baskets에 담긴 데이터만큼 쿼리가 발생한다.
BasketResponseDto basketResponseDto = new BasketResponseDto(basket.getProducts().getProductName(), basket.getProducts().getPrice(), basket.getProductQuantity());
basketResponseDtoList.add(basketResponseDto);
}
return basketResponseDtoList;
}
N+1 문제가 발생하는 근본적인 문제는 프록시 클래스로 내려받은 객체를 초기화하는 과정에서 불필요한 쿼리문이 발생하는 것이기 때문에 JPQL의 패치 조인을 통해서 하위 엔티티의 데이터를 한번에 내려받은 후 조회해서 해결할 수 있다.
@Transactional
public List<BasketResponseDto> inquiryBasket(Long userId) {
memberRepository.findById(userId).orElseThrow(
() -> new CustomException(BAD_REQUEST)
);
// Basket과 Products 한번에 조회
String query = "select b from Basket b join fetch b.products";
List<Basket> baskets = em.createQuery(query, Basket.class)
.getResultList();
List<BasketResponseDto> basketResponseDtoList = new ArrayList<>();
for (Basket basket : baskets) {
BasketResponseDto basketResponseDto = new BasketResponseDto(basket.getProducts().getProductName(), basket.getProducts().getPrice(), basket.getProductQuantity());
basketResponseDtoList.add(basketResponseDto);
}
return basketResponseDtoList;
}