[Spring] JPA의 N+1 문제

Narcoker·2025년 9월 10일

Spring

목록 보기
19/19

JPA의 N+1 문제

연관 관계가 있는 엔티티를 조회할 경우,
조회된 데이터 개수(N) 만큼 연관 관계의 조회 쿼리가 추가로 발생하는 문제

즉, 한 번의 조회로 끝날 것 같지만 실제로는 N+1번의 쿼리가 발생하여 성능 저하를 일으킨다.

발생 조건

글로벌 패치 전략이 즉시 로딩(EAGER)으로 설정된 상태에서 findAll() 메서드를 사용

글로벌 패치 전략을 지연 로딩(LAZYA)으로 설정하고 findAll()`을 실행하면
N + 1 문제가 발생하지 않는다.

이는 연관관계에 있는 엔티티를 실제 객체 대신에 프록시 객체로 생성하여 주입하기 때문

하지만 프록시 객체를 사용할 경우, 실제 데이터가 필요하여 조회하는 쿼리가 발생하고
N + 1 문제가 발생할 수 있다.

글로벌 패치 전략

EAGER
엔티티를 조회할 때 연관된 엔티티까지 즉시 함께 가져온다.
→ SQL 조인이 강제로 발생.

LAZY
연관된 엔티티는 실제로 접근할 때 쿼리가 발생한다.
→ 기본적으로 권장되는 방식.

이유

findAll() 실행 시 메인 엔티티 리스트(N개)가 먼저 조회된다.
이후 각 엔티티의 연관 관계를 EAGER 전략으로 불러오면,
엔티티마다 추가 쿼리가 발생한다.

따라서 총 쿼리 수는 1(메인 조회) + N(연관 엔티티 조회)가 되어 N+1 문제가 발생한다.

예시

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩
    private Team team;
}

@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;

    private String name;
}
List<Member> members = memberRepository.findAll();
-- members 조회
select * from member;

-- members 수만큼 team 조회
select * from team where team.id = ?;
select * from team where team.id = ?;
select * from team where team.id = ?;

Member가 100명이면, Member 조회 1번 + Team 조회 100번 = 총 101번 발생.

해결 방안

fetch join

JPQL에서 연관 엔티티를 join fetch로 함께 가져와 한 번의 쿼리로 해결한다.

fetch join
일반 join은 단순히 연관된 엔티티를 SQL 조인으로 묶어주는 역할
**fetch join은 연관된 엔티티를 함께 조회하여 영속성 컨텍스트에 로딩한다.

@Query("select m from Member m join fetch m.team")
List<Member> findAllWithTeam();
select m.*, t.*
from member m
join team t on m.team_id = t.id;

→ Member와 Team을 한 번에 가져오기 때문에 N+1 문제가 사라진다.

@EntityGraph

스프링 데이터 JPA에서 제공하는 기능으
연관 엔티티를 조회 시점에 그래프 형태로 지정하여 fetch join 효과를 낼 수 있다.

@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findAllWithTeam();
profile
열정, 끈기, 집념의 Frontend Developer

0개의 댓글