JPA의 N+1 문제 발생 이유와 해결방법(ft. Featch 전략)
JPA의 N+1 문제
N+1 문제는 연관된 데이터를 로딩할 때 발생하는 성능 문제다. 예를 들어, 하나의 엔티티(A)가 연관된 엔티티(1, 2, 3… N)를 가질 때, 한 번의 조회로 A를 가져오고, 이후 연관된 N개의 엔티티를 각각 따로 조회하는 상황이 된다.
이러한 경우, 총 N+1개의 쿼리가 발생하므로 불필요하게 많은 데이터베이스 Query가 실행된다.
// User와 Order는 일대다 관계
List<User> users = userRepository.findAll(); // User에 대한 1번의 쿼리
for(User user : users){
List<Order> orders = user.getOrders(); // 각 Order에 대한 총 N번의 쿼리
}
N+1 문제와 관련된 Fetch 전략
Lazy(지연 로딩)
- 연관된 데이터를 실제로 사용하는 시점에 데이터를 로드한다
- 즉, 필요할 때까지 쿼리를 지연시켜 불필요한 쿼리를 발생치 않도록 할 수 있다
- 기본적으로 @oneToMany 관계가 LAZY다
Eager(즉시 로딩)
- 엔티티를 조회할 때 연관된 모든 엔티티를 즉시 함께 조회한다
- 여러 연관 엔티티를 한꺼번에 가져오므로 쿼리가 많아질 때 도 있으나, 때로는 한 번의 쿼리로 데이터를 전보 가져오므로 데이터베이스 연결을 최소화할 수 있다
- 기본적으로 @ManyToOne 관계는 Eager로 설정된다
위와 같은 개념을 바탕으로, N+1문제는 주로 잘못된 LAZY 로딩 사용에 의해 바생한다. LAZY로딩의 경우 연관된 엔티티를 순차적으로 호출하며 데이터가 필요할 때마다 쿼리가 실행되며 N+1 문제가 발생하게 된다
다양한 해결방법들
(1) Eager
@Entity
public class User{
@OneToMany(fetch = FetchType.EAGER)
private List<Order> orders;
}
Eager 설정을 통해 연관된 엔티티를 항상 즉시 로딩하게 만든다. 그러면 엔티티가 조회될 때 연관된 엔티티도 무조건 함께 데이터베이스에 가져온다.
그러나 이는 앞서 언급한 것처럼 필요하지 않음에도 무조건 로딩하니 불필요한 데이터 로딩을 야기한다. 따라서 연관된 엔티티가 많다면 성능 저하를 우려해야 한다.
더불어 코드상에서 보이는 것과 같이 엔티티의 매핑 설정에 의해 동작한다. 그래서 특정 쿼리만 따로 Eager 로딩을 제어하는 게 아니라, 모든 상황에서 즉시 로딩이 강제된다는 것을 염두해야 한다.
더불어서, Eager 설정이 N+1 문제를 해결하는 것은 모든 상황에서 해당되는 게 아니다. 즉 @ManyToOne이나 @OneToOne 관계에서 Eager 로딩을 사용하면, 한 번의 쿼리로 가져오니 N+1 문제를 피하겠으나 @OneToMany, @ManyToMany 같은 다대일 관계에서는 Eager 로딩을 사용해도 여전히 N+1 문제가 발생한다.
(2) Fetch Join 활용
@Query("SELET u FROM User u JOIN FETCH u.orders")
List<User> findAllUsersWithOrders();
가장 권장되는 방법 중 하나. 이를 통해 특정 쿼리에서만 연관된 엔티티들을 한꺼번에 가져오도록 명시할 수 있다
🤫: 근데 해당 방법은 실제로 수행되는 런타임 시점에만 오류를 발견할 수 있겠네요?
🍊: 맞아요, 문자열 기반으로 작성된 JPQL 쿼리는 동작 시점에만 오류를 알 수 있죠
(3) EntityGraph 활용
@EntityGraph(attributePaths = {"orders"})
List<User> findAllUsersWithOrders();
Featch Join과 유사한데, JPA 표준 기능을 더 활용하여 연관된 엔티티를 한 번에 로드한다
이 방법도 특정 쿼리에만 Fetch 전략을 적용함으로써 유연한 해결 방법으로 뽑을 수 있다
🤫: 이 방법은 그럼 런타임에 오류를 감지해요?
🍊: 아뇹, 실행할 때 얘가 연관관계가 아니라면 속성을 찾을 수 없음에 대해 예외를 발생할 거예요.
(4) Batch Size 설정
@Entity
@BatchSize(size = 10) // 한 번에 10개의 연관 데이터를 가져오도록
public class User {
@OneToMany(fetch = FetchType.LAZY)
private List<Order> orde;
}
지연 로딩 발생 시, 한 번에 가져오는 엔티티 수를 제한하여 성능을 최적화하는방법이다
properties 파일 등으로 전역 설정도 가능하다 ( hibernate.default_batch_fetch_size =10)