N+1 문제
- 어떤 엔티티 A를 조회할 때 A와 연관된 엔티티 B가 다대일 혹은 일대다 관계로 매핑되어 있는 경우 발생
- ex) 블로그 게시글과 댓글이 있는 경우 게시글을 조회한 후 각 게시글마다 댓글을 조회하기 위한 추가 쿼리 발생
- 엔티티 A를 조회하는 1개의 쿼리 실행
- A에 연관된 B를 LAZY 로 설정해뒀다면
- 각 A에 대해 B를 따로따로 조회하기 때문에 A 수만큼 추가 쿼리 발생
- 결과적으로 총 N+1개 쿼리 실행
// 예: 하나의 팀(Team)에 여러 명의 회원(Member)이 소속되어 있는 경우
List<Member> members = em.createQuery("SELECT m FROM Member m", Member.class)
.getResultList();
for (Member member : members) {
System.out.println(member.getTeam().getName()); // 여기서 추가 쿼리 발생
}
- SELECT m FROM Member m → 1번 쿼리
- 각 member.getTeam() 호출시마다 → N번 쿼리
문제점
- 쿼리가 불필요하게 많이 실행 → 성능 저하 발생
- 데이터가 많아질수록 문제가 심각해짐
findAll 메서드의 글로벌 패치 전략 별 N + 1 문제 상황
@Entity
public class Member {
@Id
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY) // 연관관계: Member → Team
private Team team;
}
FetchType.LAZY (지연로딩, 기본값)
- @ManyToOne(fetch = FetchType.LAZY) → team 필드는 프록시 객체로 조회
- 실제 사용 시점에 team 조회를 위한 추가 쿼리 발생
- N+1 문제 발생
List<Member> members = memberRepository.findAll();
for (Member member : members) {
System.out.println(member.getTeam().getName()); // 여기서 N번 쿼리
}
FetchType.EAGER (즉시 로딩)
- @ManyToOne(fetch = FetchType.EAGER) → findAll() 시점에 team도 함께 가져오도록 SQL이 자동으로 조인
- ❗ EAGER는 항상 JOIN을 쓰는게 아니라 각각의 SELECT 쿼리를 날릴 수 있어 예측 불가능한 성능문제가 발생하기도 함
-- Member를 하나씩 조회하고, 각각의 Team도 별도로 조회하는 경우
SELECT * FROM member;
SELECT * FROM team WHERE id = ?; -- 여러 번 발생 가능
해결방법
Fetch Join
단순 조회 + 데이터 크지 않음, 전체 데이터 한번에 조회
List<Member> members = em.createQuery(
"SELECT m FROM Member m JOIN FETCH m.team", Member.class)
.getResultList();
- 한번의 쿼리로 Member와 연관된 Team까지 한번에 가져옴
- 연관관계에 있는 엔티티를 한번에 즉시 로딩하는 구문
- 데이터가 중복 조회될 수 있음 → distinct 함께 사용
- 중복 row 문제 때문에 페이징 불가
EntityGraph
관계가 복잡함(다수의 연관 관계 포함)
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
- 자동으로 fetch join처럼 동작
@BatchSize 또는 글로벌 batch
데이터 양 많음, 페이징 필요시
@BatchSize(size = 100)
@ManyToOne(fetch = FetchType.LAZY)
private Team team;
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 100
- 다수의 LAZY 로딩을 IN 쿼리 하나로 묶어 조회
- 쿼리 수는 줄지만 여전히 여러번 발생할 수 있음
DTO로 최적화 쿼리 작성
- 성능이 가장 중요할 때
정리
전략 | N + 1 문제 발생 여부 | 비고 |
---|---|---|
FetchType.LAZY | ✅ 발생 | 기본 설정, 가장 흔함 |
FetchType.EAGER | ⚠️ 조건부 발생 | 동작이 예측 어렵고, 컬렉션에서 비효율적 |
JOIN FETCH | ❌ 발생 안 함 | 명시적 쿼리 작성 필요 |
@EntityGraph | ❌ 발생 안 함 | 유지보수에 유리 |