JPA의 N + 1 문제

JPA의 N + 1 문제에 대해서 설명해주세요.

JPA N + 1 문제는 연관 관계가 설정된 엔티티를 조회할 경우에, 조회된 데이터 개수(N)만큼 연관관계의 조회 쿼리가 추가로 발생하는 현상입니다.

예를 들어, 블로그 게시글과 댓글이 있는 경우, 게시글을 조회한 후 각 게시글마다 댓글을 조회하기 위한 추가 쿼리가 발생할 수 있습니다. 이를 N + 1 문제라고 합니다.

findAll 메서드의 글로벌 패치 전략 별 N + 1 문제 상황에 대해서 설명해주세요. 🤓

참고: spring data jpa에서 제공하는 repository의 findAll 메서드

Spring Data JPA에서 findAll() 메서드 실행 시 N + 1 문제가 발생하는 이유는 글로벌 패치 전략JPQL 실행 방식 때문입니다. 이를 전략 별로 살펴보겠습니다.

1. 즉시 로딩(EAGER) 전략

즉시 로딩(EAGER) 전략을 사용하면 연관된 엔티티를 한 번에 가져오려고 합니다. 하지만 findAll()을 실행할 때 JPQL이 직접 생성하는 쿼리는 글로벌 패치 전략을 고려하지 않기 때문에 추가적인 쿼리가 발생하게 됩니다.

❌ N + 1 문제 발생 과정

List<User> users = userRepository.findAll(); 

findAll() 실행 → SELECT * FROM user (모든 User 조회)

EAGER 설정된 연관된 엔티티(예: User -> List<Post> 등)에 대해 개별적으로 조회 쿼리 실행

(모든 User에 대해 N번 실행) => 결과적으로 N + 1 문제 발생(1개의 User 조회 쿼리 + N개의 Post 조회 쿼리)

SELECT * FROM post WHERE user_id = ?

해결 방법

  • @EntityGraph 사용findAll() 실행 시 JOIN FETCH를 활용하여 한 번의 쿼리로 조회
  • FetchType을 LAZY로 설정하고, JOIN FETCH를 사용하여 필요한 경우에만 로드

2. 지연 로딩(LAZY) 전략

@OneToMany(fetch = FetchType.LAZY), @ManyToOne(fetch = FetchType.LAZY)

지연 로딩(LAZY) 전략을 사용하면 연관 엔티티를 프록시 객체로 생성하여 필요한 시점에만 조회합니다.

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

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

SELECT * FROM user (모든 User 조회)

연관된 엔티티(예: Post)는 아직 쿼리를 실행하지 않음 → Post프록시 객체로 존재

특정 User.getPosts() 호출 시 추가 쿼리 발생(이때부터 N + 1 문제 가능)

N + 1 문제 발생 가능성

지연 로딩 자체로는 N + 1 문제가 발생하지 않지만, 이후에 반복문 내에서 연관 엔티티를 조회하면 추가적인 N개의 쿼리가 실행됩니다.

for (User user : users) {
    System.out.println(user.getPosts().size()); // 여기서 각 User의 Posts를 가져오면서 추가 쿼리 발생
}

N + 1 문제는 어떻게 해결할 수 있을까요? 🤔

N + 1 문제를 해결하기 위해서는 fetch join, @EntityGraph를 사용해 볼 수 있습니다.

fetch join은 연관 관계에 있는 엔티티를 한번에 즉시 로딩하는 구문입니다.

@EntityGraph도 비슷한 효과를 만들어내며, 쿼리 메서드에 해당 어노테이션을 추가해 사용할 수 있습니다.

1.@EntityGraph 활용

@EntityGraph(attributePaths = {"posts"}) // User 조회 시 posts 함께 가져오기
@Query("SELECT u FROM User u")
List<User> findAllWithPosts();

→ 한 번의 JOIN FETCH 쿼리로 모든 데이터를 가져와서 해결

2. JOIN FETCH 사용

@Query("SELECT u FROM User u JOIN FETCH u.posts")
List<User> findAllWithPosts();

즉시로딩과 다르게 원하는 쿼리에서만 FETCH 적용 가능

3. batch_size 설정 (Hibernate @BatchSize)

@OneToMany(mappedBy = "user")
@BatchSize(size = 10) // IN 쿼리로 최적화
private List<Post> posts;

→ 여러 개의 엔티티를 한 번에 조회하여 N + 1 문제 완화

결론

  • EAGER 로딩 → findAll() 실행 시 N + 1 문제 발생 (즉시 조회하려고 개별 쿼리 실행)
  • LAZY 로딩 → findAll() 자체는 문제 없지만, 이후에 연관 엔티티를 접근할 때 N + 1 문제 발생 가능
  • 해결 방법:

    @EntityGraph

    JOIN FETCH

    @BatchSize

findAll()은 JPQL 기반으로 실행되며 글로벌 패치 전략을 고려하지 않기 때문에, 명시적으로 패치 전략을 설정해야 합니다!

JPA의 N + 1 문제
JPA의 N + 1 문제

Start the conversation