Spring

JPA: N+1 문제

개발자 김마늘 2025. 4. 20. 22:15

JPA에서 발생하는 N+1 문제의 발생 원인과 해결 방안에 대해 정리한 글입니다.

 

N+1 문제

어떤 엔티티를 조회하는 1번의 쿼리와 그 엔티티와 관련된 다른 엔티티들을 각각 조회하는 N번의 쿼리가 추가로 발생하는 현상

 

발생원인

JPA에서 연관 관계를 맺은 엔티티를 LAZY 로딩으로 설정했을 때, 연관된 엔티티에 접근하는 시점에 쿼리가 실행됩니다.

 

예를 들어 Member 엔티티와 Team 엔티티가 @ManyToOne 관계라고 가정한다면

List<Member> members = em.createQuery("SELECT m FROM Member m", Member.class).getResultList();

for (Member member : members) {
    System.out.println(member.getTeam().getName()); // 멤버가 N명이면 N번의 추가 쿼리 발생
}

 

  • 위 쿼리에서는 Member만 조회하는 쿼리 1번만 실행됩니다.
  • 하지만 .getTeam().getName()을 호출할 때마다 Team을 개별로 조회하는 쿼리가 N번 실행됩니다.
  • 즉, 1(초기에 Member 조회) + N(Member마다 Team 조회) → N+1 문제가 발생합니다.

문제가 되는 이유?

  • DB에 불필요한 부하
    • 100명의 Member를 조회하는데 연관된 팀 정보 때문에 총 101번 쿼리 실행
    • 다수의 쿼리로 인해 DB 비용이 증가
    • 전체 처리 시간 증가
    • 성능 병목

해결방안

Fetch Join 사용

SELECT m FROM Member m JOIN FETCH m.team

위 JPQL은 SQL의 JOIN 방식과 유사하게 동작하는데, Member를 조회하면서 Team까지 한 번의 쿼리로 가져옵니다.

따라서 한 번에 모두 조회하기 때문에 N+1 문제가 발생하지 않습니다.

 

EntityGraph 사용 (Spring Data JPA에서 제공)

@EntityGraph로 특정 엔티티를 조회할 때 연관된 엔티티들을 어떤 fetch 전략으로 로딩할지 지정할 수 있습니다.

JPQL을 사용하지 않고 fetch join과 같은 동작을 수행할 수 있습니다.

@EntityGraph(attributePaths = {"team"})
List<Member> findAll();

 

BatchSize 설정

 

@BatchSize는 지연 로딩을 사용하는 연관 엔티티에서 JPA가 한 번에 여러 엔티티를 모아서 한 쿼리로 조회하도록 설정하는 옵션입니다.

연관 엔티티들을 모아서 IN 쿼리로 한 번에 조회합니다. 그래서 위 방식들과는 다르게 N+1 문제를 완화하는 방법입니다.

기존: N개의 개별 SELECT 쿼리
@BatchSize 적용 시: 1번의 SELECT ... WHERE id IN (?, ?, ?, ...)