N+1 문제는 연관관계가 있는 엔티티를 조회할 때, 부모 목록을 조회하는 쿼리 1번과 각 부모의 연관 데이터를 조회하는 쿼리 N번이 추가로 실행되는 문제입니다. 예를 들어 팀 10개를 한 번에 조회한 뒤 각 팀의 회원에 접근하면, 회원 조회 쿼리가 팀마다 실행되어 총 11번의 쿼리가 발생할 수 있습니다.
지연 로딩에서는 반복문 안에서 연관 엔티티에 처음 접근할 때 주로 발생합니다. 즉시 로딩도 JPQL이 연관관계를 항상 조인해서 가져오는 것은 아니므로, 조회 이후 연관 엔티티를 채우기 위한 추가 쿼리가 실행되어 N+1 문제가 발생할 수 있습니다.
대표적인 해결 방법은 다음과 같습니다.
- Fetch Join: 필요한 연관 데이터를 한 쿼리로 함께 조회합니다.
- EntityGraph: Repository 쿼리를 유지하면서 함께 조회할 연관관계를 지정합니다.
- Batch Fetching:
default_batch_fetch_size나@BatchSize를 사용하여 여러 연관 엔티티를IN쿼리로 묶어 조회합니다. - DTO 조회: 화면이나 API에 필요한 필드만 조인하여 직접 조회합니다.
모든 연관관계를 항상 함께 조회하면 데이터 중복과 메모리 사용량이 늘 수 있으므로, 기본은 지연 로딩으로 두고 조회 용도에 맞는 방법을 선택하는 것이 좋습니다.
즉시 로딩으로 변경하면 N+1 문제가 해결되나요?
반드시 해결되지 않습니다. JPQL은 작성된 쿼리를 기준으로 실행되므로 EAGER 연관관계를 별도의 추가 쿼리로 조회할 수 있습니다. 오히려 필요하지 않은 연관관계까지 조회할 수 있어 일반적으로 LAZY를 기본으로 사용합니다.
컬렉션 Fetch Join과 페이징을 함께 사용하면 왜 문제가 되나요?
일대다 조인으로 부모 행이 자식 수만큼 중복되기 때문에 데이터베이스에서 정확히 부모 기준으로 페이징하기 어렵습니다. Hibernate가 메모리에서 페이징하면 많은 데이터를 읽을 수 있으므로, 보통 ToOne 관계만 Fetch Join하고 컬렉션은 Batch Fetching으로 조회합니다.
Batch Fetching은 N+1 문제를 어떻게 줄이나요?
연관 엔티티를 하나씩 조회하는 대신 아직 초기화되지 않은 여러 연관관계의 식별자를 모아
WHERE id IN (...)형태로 조회합니다. 쿼리를 완전히 한 번으로 만들지는 않더라도 N번의 추가 쿼리를 훨씬 적은 수로 줄일 수 있습니다.
fetch join vs batch fetching
https://domean.tistory.com/323