상세 컨텐츠

본문 제목

FetchType.LAZY/FetchType.EAGER 에서의 N+1문제?

JPA

by 코코코차 2024. 6. 1. 18:50

본문

728x90

N+1 문제란?

N+1 문제는 한 번의 쿼리로 부모 엔티티 N개를 조회한 후, 각 부모 엔티티마다 연관된 자식 엔티티들을 조회하기 위해 추가로 N개의 쿼리가 실행되는 상황을 말함. 즉, 전체적으로 (1 + N)개의 쿼리가 실행된다.


FetchType.LAZY 설정과 N+1 문제

- 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.EAGER 설정과 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();

 

장점:

  • 한 번의 쿼리로 부모와 자식 엔티티를 모두 로드하므로 쿼리 수를 크게 줄일 수 있음.
  • 성능 최적화에 효과적이며, 간단한 방법으로 N+1 문제를 해결할 수 있음.

단점:

  • 복잡한 쿼리에서 사용하기 어려울 수 있음.
  • 페이징 처리와 함께 사용하기 어려움. JPQL에서 페치 조인을 사용한 경우, 결과를 페이징하기 위해서는 추가적인 처리가 필요함.


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();

 

장점:

  • 엔티티의 특정 속성만을 선택적으로 로드할 수 있어 유연한 로딩이 가능함.
  • 쿼리 코드와 엔티티 로딩 구성을 분리할 수 있어 코드의 가독성과 유지보수성이 높아짐.

단점:

  • 기본 JPQL 쿼리에 비해 설정이 복잡할 수 있음.
  • 엔티티 그래프를 설정하고 관리하는데 추가적인 코드가 필요함.


3. Batch Fetching 설정:
   - Hibernate의 경우, `batch fetching`을 설정하여 연관 엔티티를 한 번에 로드할 수 있다.

<property name="hibernate.default_batch_fetch_size" value="10"/>

장점:

  • Hibernate에서 지원하는 기능으로, 설정만으로 연관 엔티티를 일괄 로드할 수 있어 간편함.
  • 설정을 통해 연관 엔티티를 한 번에 로드함으로써 성능 최적화를 이룰 수 있음.

단점:

  • Hibernate에 종속적인 기능으로, 다른 JPA 구현체에서는 사용할 수 없음.
  • Batch size를 적절히 설정하지 않으면 예상치 못한 성능 문제가 발생할 수 있음. 너무 작거나 너무 큰 값은 오히려 성능 저하를 유발할 수 있음.



이와 같이 N+1 문제는 로딩 방식에 상관없이 발생할 수 있으며, 적절한 방법을 사용하여 문제를 해결하고 성능을 최적화할 수 있다.

 

728x90
반응형

'JPA' 카테고리의 다른 글

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

관련글 더보기