N+1 문제는 JPA에서 연관관계 데이터를 조회할 때 SQL이 예상보다 많이 실행되는 문제입니다. 특히 FetchType.EAGER(즉시 로딩)에서 자주 발생합니다.
쉽게 말하면:
한 번 조회하면 될 것을, N개의 데이터를 추가로 조회해서 총 N+1번의 SQL이 실행되는 문제
입니다.
1. 예시로 이해하기
다음과 같은 엔티티가 있다고 가정해 보겠습니다.
Member 엔티티
@Entity
public class Member {
@Id
private Long id;
private String name;
@ManyToOne(fetch = FetchType.EAGER)
private Team team;
}
Team 엔티티
@Entity
public class Team {
@Id
private Long id;
private String name;
}
관계:
Member N : 1 Team
회원 여러 명 → 하나의 팀
2. Member 10명을 조회한다면?
코드:
List<Member> members =
em.createQuery(
"select m from Member m",
Member.class
)
.getResultList();
개발자는 이렇게 생각합니다.
회원 10명을 가져오는 SQL 하나가 실행되겠지?
실제로 실행되는 SQL:
SELECT *
FROM MEMBER;
여기까지는 1번입니다.
그런데 Member의 Team이 EAGER입니다.
즉 JPA는:
Member 객체를 만들 때 Team도 반드시 같이 가져와야 한다.
라고 생각합니다.
3. 그러면 추가 SQL이 발생합니다.
예를 들어 결과가:
| 김철수 | A팀 |
| 이영희 | A팀 |
| 박민수 | B팀 |
이라면 JPA는:
첫 번째 회원:
SELECT *
FROM TEAM
WHERE TEAM_ID = 1;
두 번째 회원:
SELECT *
FROM TEAM
WHERE TEAM_ID = 1;
세 번째 회원:
SELECT *
FROM TEAM
WHERE TEAM_ID = 2;
...
이런 식으로 실행할 수 있습니다.
결과:
회원 조회 SQL 1개
+
팀 조회 SQL N개
= N + 1 문제
4. 실제 로그로 보면
처음:
select
member0_.id,
member0_.name,
member0_.team_id
from
member member0_;
1번 실행
그 후:
select
team0_.id,
team0_.name
from
team team0_
where
team0_.id=1;
select
team0_.id,
team0_.name
from
team team0_
where
team0_.id=2;
...
여러 번 실행됩니다.
5. 그런데 왜 JPQL에서 특히 발생할까?
이 부분이 중요합니다.
예를 들어:
em.createQuery(
"select m from Member m",
Member.class
)
.getResultList();
JPQL은 그대로 SQL로 변환됩니다.
즉:
SELECT *
FROM MEMBER
만 만들어집니다.
JPA는 처음부터:
SELECT *
FROM MEMBER
JOIN TEAM
으로 만들지 않습니다.
왜냐하면 JPQL은 개발자가 작성한 쿼리이기 때문입니다.
그런데 조회된 Member를 보니:
@ManyToOne(fetch = FetchType.EAGER)
private Team team;
이 있죠.
그래서 JPA가 뒤늦게:
"아 Team도 필요하네?"
라고 판단하고 추가 조회합니다.
6. 해결 방법: LAZY 사용
그래서 실무에서는 거의 기본적으로:
@ManyToOne(fetch = FetchType.LAZY)
private Team team;
을 사용합니다.
의미:
Member를 조회할 때 Team은 가져오지 말고, 실제 사용할 때 가져와라.
예:
Member member = em.find(Member.class, 1L);
실행:
SELECT *
FROM MEMBER
WHERE ID=1;
끝.
그런데:
member.getTeam().getName();
을 호출하면 그때:
SELECT *
FROM TEAM
WHERE ID=1;
실행됩니다.
7. 그러면 LAZY에서는 N+1이 안 생기나?
아닙니다. LAZY에서도 발생합니다.
예:
List<Member> members =
em.createQuery(
"select m from Member m",
Member.class
).getResultList();
for(Member member : members){
System.out.println(member.getTeam().getName());
}
결과:
처음:
SELECT *
FROM MEMBER;
그리고 반복문:
SELECT *
FROM TEAM WHERE ID=1;
SELECT *
FROM TEAM WHERE ID=2;
SELECT *
FROM TEAM WHERE ID=3;
결국:
1 + N
입니다.
8. 해결 방법: Fetch Join
가장 대표적인 해결책입니다.
JPQL:
select m
from Member m
join fetch m.team
그러면 SQL:
SELECT
M.*,
T.*
FROM MEMBER M
JOIN TEAM T
ON M.TEAM_ID = T.ID
한 번에 가져옵니다.
결과:
SQL 1번
정리
| EAGER | 조회 시 무조건 연관 객체 로딩 → N+1 위험 큼 |
| LAZY | 실제 사용할 때 조회 → 기본 추천 |
| Fetch Join | 필요한 경우 한 번에 조회 → N+1 해결 |
| EntityGraph | Fetch Join과 비슷한 해결 방법 |
실무 JPA에서는 보통:
@ManyToOne(fetch = FetchType.LAZY)
를 기본으로 사용하고, 조회 화면이나 API에서 필요한 경우:
join fetch
를 사용해서 N+1 문제를 해결합니다.
'Spring&IntelliJ' 카테고리의 다른 글
| protected기본생성자를 사용하면 왜 개발자는 함부로 생성하지 못하고 JPA같은 프레임워크는 접근이 가능한거야? (0) | 2026.06.16 |
|---|---|
| @Embeddable에 대해서 (0) | 2026.06.16 |
| @Inheritance에 대해서 (3가지 전략) (0) | 2026.06.16 |
| cascade = CascadeType.ALL은 어떨때 주로 쓰이고 왜 쓰여? (0) | 2026.06.16 |
| h2 파일 생성 실패시, Database not found 애러 해결방법 (0) | 2026.06.05 |