JPA · 2025-11-05 · 1분 읽기

JPA N+1 문제, 제대로 이해하고 해결하기

목차
  • 1. N+1이 뭔가
  • 2. 왜 발생하나
  • 3. Fetch Join으로 해결
  • 4. @EntityGraph
  • 5. Batch Size로 해결
  • 6. 어떤 걸 써야 하나

1. N+1이 뭔가

Post를 조회했는데 쿼리가 N+1개 나가는 현상이다.

// 이 코드 한 줄이
List<Post> posts = postRepository.findAll();
 
// 이런 쿼리를 만들어낸다
// SELECT * FROM post          → 1번
// SELECT * FROM comment WHERE post_id = 1   → N번
// SELECT * FROM comment WHERE post_id = 2
// ...

2. 왜 발생하나

기본 fetch 전략이 LAZY이기 때문이다. 연관 엔티티는 실제로 접근할 때 쿼리가 나간다.

@Entity
public class Post {
    @OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
    private List<Comment> comments;
}

EAGER로 바꿔도 해결이 안 된다. JPQL은 fetch 전략을 무시하고 별도 쿼리를 날린다.

3. Fetch Join으로 해결

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

쿼리 한 방에 해결된다.

SELECT p.*, c.*
FROM post p
INNER JOIN comment c ON c.post_id = p.id

단, 컬렉션 Fetch Join은 페이징 불가하다. HibernateJpaDialect: HHH90003004 경고와 함께 인메모리 페이징이 일어난다.

4. @EntityGraph

@EntityGraph(attributePaths = {"comments", "tags"})
List<Post> findAll();

Fetch Join보다 선언적으로 쓸 수 있다. 하지만 여러 컬렉션을 동시에 사용하면 MultipleBagFetchException이 발생한다.

5. Batch Size로 해결

컬렉션 페이징이 필요할 때 쓰는 방법이다.

@BatchSize(size = 100)
@OneToMany(mappedBy = "post")
private List<Comment> comments;

또는 전역 설정:

spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 100

N+1 대신 IN 쿼리로 묶어서 처리한다.

-- N+1 대신
SELECT * FROM comment WHERE post_id IN (1, 2, 3, ..., 100)

6. 어떤 걸 써야 하나

상황해결책
단건 조회, 컬렉션 없음Fetch Join
목록 조회, 페이징 없음Fetch Join
목록 조회, 페이징 있음BatchSize
복잡한 조인 필요QueryDSL + DTO 프로젝션

'JPA ' 카테고리의 다른 글

  • 이 카테고리에 다른 글이 없습니다.