JPA 에서 성능 이슈 로 발생하는 문제 n+1 문제입니다 조인이 안해서 따로 조회 해서 발생하는 문제입니다.
오늘은 로딩 전략(Eager/Lazy)별로 N+1이 발생하는 원인과 그 결과, 그리고 해결책까지 순서대로 정리해 보겠습니다
1. 상황 가정
- **Member(회원)**와 **Team(팀)**은 N:1 관계입니다.
- 회원 10명을 조회하려는데, 각 회원은 서로 다른 팀에 소속되어 있습니다.
2. CASE 1: 즉시 로딩 (Eager)일 때
JPA에게 "멤버 조회할 때 팀도 무조건 같이 가져와!"라고 설정한 경우입니다.
// Member 엔티티
@ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩
@JoinColumn(name = "team_id")
private Team team;
// 단순히 전체 멤버만 조회함
List<Member> members = memberRepository.findAll();
🧐 원인 분석
- JPQL(select m from Member m)은 글로벌 Fetch 전략(EAGER)을 신경 쓰지 않고, SQL을 생성합니다. -> 일단 Member만 조회
- 데이터를 다 가져온 뒤, JPA가 엔티티를 확인해보니 Team이 EAGER입니다.
- "어? 팀도 당장 필요하네?"라고 판단하고, 조회된 멤버 수(N)만큼 팀 조회 쿼리를 추가로 날립니다.
💥 결과 (SQL 로그)
-- 1. Member 조회 (1번)
select * from member;
-- 2. 각 Member의 Team을 채우기 위해 급하게 추가 쿼리 발송 (N번)
select * from team where id = 1;
select * from team where id = 2;
...
select * from team where id = 10;
3. CASE 2: 지연 로딩 (Lazy)일 때
"팀은 실제로 사용할 때 가져올게"라고 미루는 설정입니다. 실무에서 권장하는 방식이지만, N+1을 근본적으로 막지는 못합니다.
// Member 엔티티
@ManyToOne(fetch = FetchType.LAZY) // 지연 로딩
@JoinColumn(name = "team_id")
private Team team;
💻 실행 코드
List<Member> members = memberRepository.findAll(); // 1. 여기까지는 괜찮음
for (Member member : members) {
// 2. 루프를 돌며 팀의 이름을 확인(사용)하는 순간!
System.out.println(member.getTeam().getName());
}
🧐 원인 분석
- findAll() 시점에는 Member만 가져오고, Team 자리에는 **프록시(가짜 객체)**를 채워둡니다.
- 하지만 루프 안에서 member.getTeam().getName()으로 실제 데이터에 접근하려고 하면,
- 프록시는 "나는 데이터가 없는데?" 하고 DB에 쿼리를 날려 데이터를 가져옵니다(초기화). 이게 루프 횟수만큼 반복됩니다.
-- 1. Member 조회 시점 (1번)
select * from member;
-- ... (잠시 후) ...
-- 2. 루프 돌며 getTeam().getName() 호출 시점 (N번)
select * from team where id = 1;
select * from team where id = 2;
...
4. 해결책 (Solution)
결국 Eager든 Lazy든 **"따로 조회한다"**는 점이 문제입니다. 이를 해결하려면 **"처음부터 조인(Join)해서 가져와라"**라고 명시해야 합니다.
방법 1: Fetch Join (가장 일반적)
직접 JPQL을 작성하여 조인을 명시합니다.
@Query("select m from Member m join fetch m.team")
List<Member> findAllJoinFetch();
결과: INNER JOIN을 사용하여 쿼리 1방에 Member와 Team을 모두 가져옵니다.
방법 2: @EntityGraph (가장 깔끔함)
JPQL 작성 없이 어노테이션으로 해결합니다.
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
결과: LEFT OUTER JOIN을 사용하여 쿼리 1방에 모두 가져옵니다.
'개발 공부 > Java-Spring' 카테고리의 다른 글
| Spring의 핵심, IoC와 DI: 제어의 역전과 의존성 주입 완벽 가이드 (0) | 2026.01.23 |
|---|---|
| 자바 접근 제어자및 스프링부트에서 사용예시 (0) | 2026.01.20 |
| 불변의 객체 의 정의 (0) | 2025.12.10 |
| spring ai 해보기 (0) | 2025.12.06 |
| 객체 지향 프로그래밍(OOP)의 특징 (0) | 2025.11.22 |