Spring Data JPA에서 목록 조회 한 번에 쿼리가 수백 개 나간 이유
문제 발생
주문 목록 100건을 조회하는 API인데, 로그를 보니 쿼리가 101번(주문 조회 1번 + 각 주문의 고객 정보 조회 100번) 발생하고 있었습니다.
List<Order> orders = orderRepository.findAll(); // 쿼리 1번
for (Order order : orders) {
System.out.println(order.getCustomer().getName()); // 각 반복마다 추가 쿼리
}원인 분석
@ManyToOne/@OneToOne 연관관계를 기본값 또는 FetchType.LAZY로 설정하면, JPA는 연관 엔티티를 즉시 가져오지 않고 프록시 객체만 넣어둡니다. order.getCustomer().getName()처럼 실제로 그 프록시의 필드에 접근하는 순간, JPA는 그제서야 SELECT * FROM customer WHERE id = ? 쿼리를 추가로 날립니다.
목록을 순회하면서 각 항목마다 이 접근이 일어나면, 전체 목록 조회 1번(N을 가져오는 쿼리) + 각 항목의 연관 엔티티 조회 N번이 발생합니다 — 이를 N+1 문제라고 부릅니다. 항목이 100개면 쿼리가 101번 나가는 것입니다.
해결 방안
- Fetch Join: JPQL에서
JOIN FETCH로 명시적으로 연관 엔티티를 한 번의 쿼리로 함께 가져옵니다.
@Query("SELECT o FROM Order o JOIN FETCH o.customer")
List<Order> findAllWithCustomer();- @EntityGraph: 메서드 단위로 특정 연관관계만 즉시 로딩하도록 지정할 수 있어 JPQL을 직접 안 써도 됩니다.
@EntityGraph(attributePaths = {"customer"})
List<Order> findAll();- 배치 사이즈 설정(
@BatchSize또는spring.jpa.properties.hibernate.default_batch_fetch_size): N개의 개별 쿼리 대신IN절로 묶어서 조회해 쿼리 수를 줄입니다. Fetch Join만큼 완전히 없애지는 못하지만 쿼리 수를 크게 줄일 수 있습니다. - 컬렉션(
@OneToMany)에 Fetch Join을 쓸 때는 페이지네이션과 함께 쓰면 메모리에서 페이징 처리를 하게 되는 함정이 있으므로("HHH000104" 경고),@OneToManyFetch Join과Pageable을 같이 쓰지 않도록 주의합니다. - 이런 문제는 로그로 실제 쿼리 발생 횟수를 확인해야 발견할 수 있습니다 —
spring.jpa.show-sql=true나 P6Spy 같은 도구로 실제 쿼리 로그를 확인하는 습관이 필요합니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.