
N+1 문제는 한 번의 쿼리로 부모 엔티티 N개를 조회한 후, 각 부모 엔티티마다 연관된 자식 엔티티들을 조회하기 위해 추가로 N개의 쿼리가 실행되는 상황을 말함. 즉, 전체적으로 (1 + N)개의 쿼리가 실행된다.
- FetchType.LAZY: 엔티티를 처음 조회할 때 연관된 엔티티를 즉시 로드하지 않고, 실제로 해당 연관 엔티티가 필요할 때(접근할 때) 쿼리를 실행하여 로드하는 방식
예를 들어, `Parent`와 `Child` 엔티티가 `OneToMany` 관계에 있으며, `Parent` 엔티티가 `FetchType.LAZY`로 설정되어 있다고 가정
@Entity
public class Parent {
@OneToMany(fetch = FetchType.LAZY, mappedBy = "parent")
private List<Child> children;
}
이제 `Parent` 엔티티 목록을 조회하면, 부모 엔티티를 가져오는 쿼리가 먼저 실행
List<Parent> parents = entityManager.createQuery("SELECT p FROM Parent p", Parent.class).getResultList();
여기까지는 문제가 없음. 하지만, 이후 각 부모 엔티티에 대해 자식 엔티티를 접근하면, 각 접근마다 쿼리가 실행
for (Parent parent : parents) {
List<Child> children = parent.getChildren(); // 각 parent에 대해 추가 쿼리 실행
}
이러한 접근 방식 때문에 다음과 같은 쿼리가 실행
1. 부모 엔티티 목록을 가져오는 1개의 쿼리.
2. 각 부모 엔티티에 대해 자식 엔티티를 가져오는 N개의 쿼리.
즉, 부모 엔티티가 N개라면 전체적으로 1 + N개의 쿼리가 실행되며, 이것이 바로 N+1 문제이다.
`FetchType.LAZY` 뿐만 아니라 `FetchType.EAGER`를 사용해도 N+1 문제가 발생할 수 있다. 이는 로딩 시점의 문제일 뿐, 근본적으로는 연관된 엔티티를 가져오는 방식에서 기인하는 문제다.
즉시 로딩 (FetchType.EAGER)에서의 N+1 문제
`FetchType.EAGER`를 사용하면 연관된 엔티티를 즉시 로드하게 된다. 이는 부모 엔티티를 조회할 때마다 해당 부모 엔티티와 연관된 자식 엔티티도 한 번에 가져온다는 의미다.
예시
예를 들어, `Parent`와 `Child` 엔티티가 `OneToMany` 관계에 있으며, `Parent` 엔티티가 `FetchType.EAGER`로 설정되어 있다고 가정하자.
@Entity
public class Parent {
@OneToMany(fetch = FetchType.EAGER, mappedBy = "parent")
private List<Child> children;
}
이제 `Parent` 엔티티 목록을 조회하면, 부모 엔티티를 가져오는 쿼리와 함께 각 부모 엔티티에 대해 자식 엔티티를 가져오는 쿼리가 실행된다.
List<Parent> parents = entityManager.createQuery("SELECT p FROM Parent p", Parent.class).getResultList();
여기서 발생하는 N+1 문제를 자세히 살펴보면 다음과 같다:
1. 부모 엔티티 목록을 가져오는 1개의 쿼리.
2. 부모 엔티티에 대해 자식 엔티티를 가져오는 N개의 쿼리.
즉, 부모 엔티티가 N개라면 전체적으로 1 + N개의 쿼리가 실행된다.
`FetchType.LAZY`와 `FetchType.EAGER` 모두 N+1 문제가 발생할 수 있지만, 문제의 발생 시점이 다르다:
- 지연 로딩 (FetchType.LAZY): 부모 엔티티를 처음 조회할 때는 자식 엔티티를 로드하지 않다가, 자식 엔티티에 접근할 때마다 쿼리가 실행된다.
- 즉시 로딩 (FetchType.EAGER): 부모 엔티티를 조회할 때 연관된 모든 자식 엔티티를 한 번에 로드하려고 시도하므로, 처음 조회 시점에 N+1 문제가 발생한다.
두 경우 모두 N+1 문제를 해결하기 위해 다음과 같은 방법을 사용할 수 있다.
1. Fetch Join 사용:
- JPQL에서 `JOIN FETCH`를 사용하여 한 번의 쿼리로 부모와 자식 엔티티를 모두 로드한다.
List<Parent> parents = entityManager.createQuery(
"SELECT p FROM Parent p JOIN FETCH p.children", Parent.class).getResultList();
장점:
단점:
2. Entity Graph 사용:
- JPA의 `Entity Graph` 기능을 사용하여 필요한 연관 엔티티를 로드한다.
EntityGraph<Parent> graph = entityManager.createEntityGraph(Parent.class);
graph.addAttributeNodes("children");
List<Parent> parents = entityManager.createQuery("SELECT p FROM Parent p", Parent.class)
.setHint("javax.persistence.fetchgraph", graph)
.getResultList();
장점:
단점:
3. Batch Fetching 설정:
- Hibernate의 경우, `batch fetching`을 설정하여 연관 엔티티를 한 번에 로드할 수 있다.
<property name="hibernate.default_batch_fetch_size" value="10"/>
장점:
단점:
이와 같이 N+1 문제는 로딩 방식에 상관없이 발생할 수 있으며, 적절한 방법을 사용하여 문제를 해결하고 성능을 최적화할 수 있다.
| JPA 캐시/ 1차 캐시, 2차 캐시 (1) | 2024.06.08 |
|---|---|
| JPA 지연로딩, 즉시로딩, N+1문제 (1) | 2024.06.08 |
| 연관 관계(OneToMany, ManyToOne) (0) | 2024.06.01 |
| JPA에서 Cascade Type을 지정하지 않으면? (0) | 2024.06.01 |
| [JPA/Hibernate] Flush란? (0) | 2024.05.30 |