1. N+1 문제가 발생하는 원인

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;
    }

발생한 쿼리

Untitled

2. N+1 문제를 해결하는 방법

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;
    }

발생한 쿼리