[JAVA] N+1 문제와 해결 방법 – Fetch Join과 @EntityGraph로 해결하기

JPA를 실무에 도입하면 많은 개발자가 가장 먼저 마주하는 성능 문제 중 하나가 N+1 문제입니다. 간단한 데이터 조회 쿼리처럼 보이지만, 실제 서비스 환경에서는 심각한 성능 저하로 이어질 수 있습니다. N+1 문제의 발생 원인부터 실전 해결책인 Fetch Join@EntityGraph를 활용하는 을 정리했습니다.

 

 

1. N+1 문제란 무엇인가?

간단히 말해, 1번의 조회 쿼리(N) 이후에 관련된 엔티티를 조회하기 위한 추가 쿼리(+N)가 발생하는 문제입니다. 예를 들어 게시글(Post) 리스트를 조회하고, 각 게시글의 댓글(Comment) 리스트를 같이 가져오려고 할 때 다음과 같은 쿼리 구조가 됩니다.

List<Post> posts = postRepository.findAll();
for (Post post : posts) {
    System.out.println(post.getComments().size());
}

위 코드의 결과는 다음과 같습니다:

  • 1번 쿼리: select * from post
  • N번 쿼리: select * from comment where post_id = ? 가 게시글 수만큼 반복

즉, 게시글이 100개라면 총 101개의 쿼리가 실행됩니다.

 

2. 왜 발생하는가?

기본적으로 JPA는 @OneToMany 등의 연관관계를 Lazy Loading으로 설정합니다. 따라서 해당 연관 필드에 실제 접근이 일어나기 전까지는 데이터를 로딩하지 않죠. 하지만 루프 안에서 호출되면 그때마다 SQL 쿼리가 발생하게 됩니다.

 

3. 해결 방법 1: Fetch Join

기본 사용법

@Query("SELECT p FROM Post p JOIN FETCH p.comments")
List<Post> findAllWithComments();

JOIN FETCH를 사용하면 한 번의 쿼리로 연관 엔티티를 즉시 로딩할 수 있습니다. 다만 1:N 관계에서는 중복 row가 생기므로 distinct 키워드를 함께 사용하거나 애플리케이션에서 중복 제거가 필요합니다.

@Query("SELECT DISTINCT p FROM Post p JOIN FETCH p.comments")
List<Post> findAllWithCommentsDistinct();

주의 사항

  • JOIN FETCH는 2개 이상의 연관관계를 동시에 가져올 경우 조인 결과가 폭발할 수 있음
  • 페이징 처리 시 fetch join은 부적절 (Spring Data JPA에서 예외 발생 가능)

 

4. 해결 방법 2: @EntityGraph

@EntityGraphFetch 전략을 메서드 단위에서 명시적으로 조정할 수 있게 해줍니다.

@EntityGraph(attributePaths = "comments")
List<Post> findAll();

JPA 구현체가 내부적으로 fetch join과 유사하게 처리하지만, JPQL 작성 없이도 fetch 적용이 가능하다는 장점이 있습니다.

 

다중 연관 필드 로딩

@EntityGraph(attributePaths = {"comments", "author"})
List<Post> findByTitleContaining(String keyword);

 

페이징과 함께 사용 가능

@EntityGraphPageable과도 잘 호환되므로 페이징 처리가 필요한 경우 매우 유용합니다.

 

5. 정리: 언제 어떤 방법을 쓸까?

조건 추천 방법
단건 조회 + 연관 관계 필요 Fetch Join
다건 조회 + 페이징 필요 @EntityGraph
1:N 관계의 리스트 반환 Fetch Join + DISTINCT 처리 필요
JPA Query 없이 사용 @EntityGraph

 

마무리

N+1 문제는 초반에는 간과되기 쉽지만, 트래픽이 많아질수록 DB 부하와 응답 지연으로 이어지는 치명적인 병목 요소입니다. JOIN FETCH@EntityGraph는 각각 장단점이 있으므로 상황에 맞게 선택해야 하며, 무조건 EAGER 전략을 쓰는 것은 권장드리고 싶지 않습니다.