우당탕탕

[Spring] JPA N+1 문제 해결법 ( 원인부터 해결까지 ) 본문

Tech/Spring

[Spring] JPA N+1 문제 해결법 ( 원인부터 해결까지 )

모찌모찝 2025. 4. 25. 17:12
JPA N+1 문제 해결법

 

이번 포스팅에서는 JPA에서 자주 발생하는 성능 이슈인 N+1 문제와 이를 해결하는 방법들을 정리해 보려고 합니다. 

Spring Data JPA

N+1 문제란?

N+1 문제란, JPA로 엔티티를 조회할 때 연관된 엔티티를 추가로 조회하면서 불필요하게 많은 쿼리가 실행되는 현상을 말합니다. 예를 들어, 게시글 10개를 조회할 때 각 게시글의 작성자 정보를 연관엔티티로 가져온다면, 게시글 목록(10개의 게시글)을 가져오는 쿼리 1번 + 작성자 정보(게시글당 1번씩) 쿼리 10번, 총 11번의 쿼리가 실행되는 것이 대표적인 N+1 문제입니다.

N+1 문제가 왜 생길까?

JPA에서 연관된 엔티티는 기본적으로 지연로딩(LAZY) 방식입니다. 즉, 실제로 해당 데이터를 사용할 때마다 추가 쿼리가 실행되는 방식입니다.

N+1 문제 해결법

1. Fetch Join 사용하기

Fetch Join 사용은 가장 많이 사용되는 방법입니다. JPQL에서 Fetch Join을 사용하면 연관된 데이터를 한 번의 쿼리로 가져올 수 있습니다.

@Query("select t from Team t join fetch t.members")
List<Team> findAllWithMembers();

 

장점: 한 번의 쿼리로 모든 데이터 조회 가능
단점: 1:N 관계에서 중복 데이터가 생길 수 있고, 페이징에서 사용이 제한

2. @EntityGraph 사용하기

@EntityGraph는 JPA에서 제공하는 기능으로, 복잡한 JPQL 없이도 연관 데이터를 한 번에 가져올 수 있습니다. 

@EntityGraph(attributePaths = {"members"}) 
List<Team> findAll();
장점: 코드가 간단하고, 페이징과 같이 쓸 수 있음
단점: 복잡한 쿼리에서는 한계가 있음

3. Batch Size 설정하기

JPA 설정이나 어노테이션으로 한 번에 여러 연관 데이터를 IN 쿼리로 묶어서 가져오는 방법입니다.

@BatchSize(size = 100) 
@OneToMany(mappedBy = "team") 
private List<Member> members;

장점: 쿼리 수를 줄일 수 있으며, 페이징과 함께 사용 가능
단점: 쿼리가 완전히 1번만 실행되지는 않음 ( IN 쿼리로 여러 개씩 묶어서 실행 )

4. SUBSELECT 전략

Hibernate에서 지원하는 방식으로, 메인 데이터를 먼저 조회한 뒤 연관데이터는 한 번의 서브쿼리로 가져오는 방식입니다.

@OneToMany(fetch = FetchType.LAZY) 
@Fetch(FetchMode.SUBSELECT) 
private List<Member> members;

장점: N+1문제 해결, 페이징과 함께 사용 가능
단점: 데이터가 너무 많으면 성능 저하가 발생할 수 있음

상황별 사용 전략

연관 데이터까지 한 번에 조회 fetch join, @EntityGraph
페이징이 필요할 때 Batch Size, SUBSELECT
연관 데이터가 아주 많을 때 Batch Size, SUBSELECT, 또는 DTO로 분리 조회
 

마무리

N+1 문제는 JPA를 쓴다면 꼭 한 번은 마주치는 문제입니다. 쿼리 로그를 꼭 확인하면서, 불필요하게 쿼리가 반복되는 것 같다면 위 방법들 중 하나를 적용해서 해결해 보세요!
개인적으로는 단순 조회 시에는 Fetch Join , 페이징이 필요하다면 @EntityGraph나 Batch Size 방법을 추천드립니다.

 

Comments