본 문서는 Spring Data JPA + Hibernate를 전제로 설명합니다.
select u from User u)로 N건의 엔티티를 조회한 뒤, 각 엔티티의 연관 엔티티를 접근하는 순간 추가 쿼리 N건이 발생하는 현상입니다.User ↔ Post(OneToMany) 관계에서 모든 User를 조회한 후, 각 사용자에 대해 posts를 접근하면 사용자 수만큼 select ... from post where user_id = ?가 반복 실행됩니다.findAll() 과 글로벌 패치 전략(EAGER/LAZY)Spring Data JPA의 Repository#findAll()은 내부적으로 다음 JPQL을 실행합니다.
select u from User u
중요 포인트는 JPQL은 매핑의 글로벌 fetch 전략을 고려하지 않고 실행된다는 점입니다. 이후 연관 필드 접근 시점에 따라 N+1이 발생합니다.
findAll()로 User 목록을 읽은 뒤, Hibernate는 즉시로딩 규칙에 따라 각 연관을 추가 쿼리로 가져오려 시도합니다.findAll()은 루트만 조회하고, 연관 컬렉션은 프록시(초기화 전)로 주입됩니다.u.getPosts() 등 실제 접근이 발생하면 그때 추가 쿼리가 실행됩니다.결론: 글로벌 패치 전략만으로 N+1을 근본 해결할 수 없습니다. “어떤 시점에 무엇을 로드할지”를 쿼리 단위로 통제해야 합니다.
@Entity
class User {
@Id @GeneratedValue Long id;
String name;
@OneToMany(mappedBy = "user")
List<Post> posts = new ArrayList<>();
}
@Entity
class Post {
@Id @GeneratedValue Long id;
String title;
@ManyToOne(fetch = FetchType.LAZY)
User user;
}
// 서비스
List<User> users = userRepository.findAll(); // select u from User u
for (User u : users) {
// 여기서 접근하는 순간 사용자 수만큼 추가 쿼리
System.out.println(u.getPosts().size());
}
fetch join (가장 직접적)연관을 한 번에 로드합니다.
// Repository
@Query("""
select distinct u
from User u
left join fetch u.posts
""")
List<User> findAllWithPosts();
distinct 필요(엔티티 중복 제거).@EntityGraphJPQL 작성 없이 로딩 그래프를 선언적으로 지정합니다.
@EntityGraph(attributePaths = {"posts"}, type = EntityGraph.EntityGraphType.FETCH)
@Query("select u from User u")
List<User> findAllWithPostsByEntityGraph();
// 또는 메서드 이름 기반에도 적용 가능
@EntityGraph(attributePaths = {"posts"})
List<User> findAll();
@BatchSize, 글로벌 hibernate.default_batch_fetch_size)LAZY 컬렉션/프록시 초기화 시 IN 쿼리로 묶어서 가져옵니다.
@Entity
@BatchSize(size = 50) // 또는 application 설정으로 글로벌 적용
class User { ... }
ceil(N / batch) 로 쿼리 수 감소.select u ... limit ..., 이후 select p ... where user_id in (...) 로 한 번 더 가져와 Map으로 결합.@Fetch(FetchMode.SUBSELECT) 등으로 동일 세션에서 로딩된 컬렉션을 한 번 더의 서브쿼리로 모아 가져옴.fetch join 또는 @EntityGraphfindAll()과 조합findAll()은 select u from User u만 수행 → N+1 위험@EntityGraph 부여default_batch_fetch_size로 방어선 구축// 예시: 엔티티 그래프 부착
public interface UserRepository extends JpaRepository<User, Long> {
@Override
@EntityGraph(attributePaths = {"posts"})
List<User> findAll();
// 또는 별도 메서드로 분리
@EntityGraph(attributePaths = {"posts"})
List<User> findAllWithPosts();
}