본문 바로가기
카테고리 없음

JPA에서 N+1 문제를 해결하기 위한 방법

by jhdevtrace 2024. 10. 11.

N+1이란?

제가 사용한 스프링의 JPA 프레임워크의 지연 로딩 방식을 사용한 경우에 생기는 성능 문제입니다.

발생 원인은, 지연 로딩이 처음에는 연관된 데이터를 가져오지 않다가, 실제로 그 데이터를 참조할 때 쿼리가 추가로 실행되게 됩니다.

 

N+1을 좀 더 자세히 풀어보자면,

1은 처음 실행한 쿼리입니다. 무조건 발생되는 쿼리이고,

이 뒤에 추가로 쿼리를 써야 되면서, 추가로 발생하는 쿼리의 수를 N이라고 부릅니다.

그래서 추가로 발생되는 N을 최소화하는 것이 N+1 해결 방법이라고 할 수 있습니다.

 

 

N+1을 해결하는 방법은?

여러 방법이 있는 것으로 알고 있지만, 제가 사용해본 방법으로는, fetch Join 방법과 BatchSize 방법을 사용해봤습니다.

 

 

fetch Join

상황에 따라서 완벽히 N+1문제를 해결할 수 있습니다. 근데 해당 엔티티가 다른 엔티티와 ToMany 연관이 많으면서, fetch join을 여러 번 사용한 경우, MultipleBagFetchException이 발생할 수 있습니다.

  • fetch Join 특징연관된 엔티티나 컬렉션을 한 번에 같이 조회할 수 있도록 지원함
    • ToMany를 여러개 Fetch Join 할 경우 발생
    • To One은 몇개든 Fetch Join 가능
    • ToMany는 1개만 Fetch Join 가능
  • MultipleBagFetchException
  • Fetch Join은 실제 SQL에 있는 Join의 종류가 아니며, JPQL에서 성능 최적화를 위해 제공하는 Join 기능

 

BatchSize

발생하는 여러 개의 쿼리를 줄이기 위해 연관된 엔티티를 일정 크기로 묶어서 가져오는 방법입니다.

해당 크기는 조절할 수 있습니다. 지정한 크기보다 불러오는 데이터 수가 적으면 메모리 낭비가 발생하게 되며, 크기보다 넘치는 데이터를 가져오게 되는 경우, 성능이 저하될 수 있습니다. 그래서 적절히 절충할 수 있는 크기를 정하는 것이 좋습니다.

 

 

 

 

 

 

 

 

==============================================

 

 

 

 

N+1 문제란?

  • N+1 문제는 한 번의 쿼리(1)를 통해 조회된 엔티티들이 연관된 엔티티를 조회할 때, 각각 추가로 N개의 쿼리가 발생하는 문제를 말합니다.
  • 예를 들어, 부모 엔티티 10개를 조회할 때, 각각의 부모 엔티티가 자식 엔티티와 연관되어 있으면, 자식 엔티티를 조회하는 N개의 쿼리가 추가로 발생하게 됩니다.

N+1 문제를 해결하는 방법

  1. Fetch Join 사용
    • 개념: Fetch Join은 JOIN FETCH 구문을 사용하여 연관된 엔티티를 한 번의 쿼리로 함께 가져오는 방법입니다.
    • 장점: 여러 쿼리가 발생하는 것을 방지하고, 연관된 엔티티를 한 번에 조회할 수 있습니다.
    • 예시:
    • @Query("SELECT p FROM Parent p JOIN FETCH p.children") List<Parent> findAllWithChildren();
    • 이 방식은 부모 엔티티와 자식 엔티티를 한 번의 쿼리로 조회하여 N+1 문제를 해결합니다.
  2. EntityGraph 사용
    • 개념: @EntityGraph는 JPA에서 연관된 엔티티를 조회할 때 Fetch 전략을 지정할 수 있는 어노테이션입니다.
    • 장점: 코드 내에서 간단하게 Fetch Join을 적용할 수 있으며, 상황에 따라 동적으로 Fetch 전략을 지정할 수 있습니다.
    • 예시:
    • @EntityGraph(attributePaths = {"children"}) @Query("SELECT p FROM Parent p") List<Parent> findAllWithChildren();
    • 이 방식으로 연관된 엔티티를 함께 가져올 수 있습니다.
  3. BatchSize 사용
    • 개념: @BatchSize는 연관된 엔티티들을 일정 단위로 묶어서 조회하는 방식입니다. N개의 쿼리가 발생하지 않도록 배치 단위로 가져옵니다.
    • 장점: 연관된 엔티티가 많을 경우, 한번에 여러 엔티티를 묶어서 가져올 수 있어 N+1 문제를 완화할 수 있습니다.
    • 예시:
    • @OneToMany @BatchSize(size = 10) private List<Child> children;
    • 이 방식은 자식 엔티티를 한 번에 묶어서 조회하므로 N+1 문제를 해결합니다.
  4. Lazy Loading과 적절한 트랜잭션 관리
    • Lazy Loading을 사용할 때, 트랜잭션 범위를 적절히 관리해 N+1 문제를 방지할 수 있습니다. Lazy 로딩이 트랜잭션 범위 내에서만 동작하기 때문에, 필요한 시점에 데이터가 로드되도록 트랜잭션을 유지하는 것이 중요합니다.
    • 하지만, Lazy Loading만으로는 완벽히 N+1 문제를 해결하기 어렵기 때문에 Fetch Join이나 BatchSize와 함께 사용하는 것이 좋습니다.

정리

  • Fetch JoinEntityGraph는 가장 많이 사용되는 N+1 문제 해결 방법입니다. 한 번의 쿼리로 연관된 데이터를 모두 조회할 수 있기 때문에 성능 최적화에 도움이 됩니다.
  • BatchSize는 연관된 엔티티를 일괄 처리함으로써 쿼리의 수를 줄여 N+1 문제를 해결할 수 있습니다.

이 방법들을 사용하면 JPA에서 발생하는 N+1 문제를 효과적으로 해결할 수 있습니다냥!

================================================================

Fetch Join과 BatchSize는 JPA에서 성능을 최적화하고, N+1 문제를 해결하는 데 자주 사용되는 중요한 개념입니다. 각각의 개념과 사용 방법을 좀 더 구체적으로 설명할게요.

1. Fetch Join

개념

  • Fetch Join은 SQL의 JOIN 구문을 사용하여 연관된 엔티티를 한 번에 가져오는 방법입니다. JPA에서 기본적으로 EAGER나 LAZY 로딩 전략이 있지만, 이 두 방식은 각각의 장단점이 있기 때문에 필요한 경우 JOIN FETCH로 명시적으로 연관된 엔티티를 함께 조회할 수 있습니다.

장점

  • 한 번의 쿼리로 연관된 데이터를 가져옴: JOIN FETCH를 사용하면 N+1 문제가 해결됩니다. 여러 번의 쿼리를 발생시키지 않고, 연관된 엔티티 데이터를 한 번의 쿼리로 가져오기 때문에 성능을 최적화할 수 있습니다.

사용 방법

  • 연관된 엔티티를 조회할 때 JOIN FETCH를 사용하여 관련 엔티티도 함께 로드합니다.
  • 예를 들어, Parent 엔티티와 Child 엔티티가 1:N 관계에 있을 때, Parent와 함께 Child를 Fetch Join으로 가져오는 쿼리는 다음과 같습니다:
@Query("SELECT p FROM Parent p JOIN FETCH p.children")
List<Parent> findAllWithChildren();

  • 이 쿼리는 Parent 엔티티를 조회할 때, 관련된 Child 엔티티를 한 번의 쿼리로 함께 가져오게 됩니다. 이렇게 하면 N+1 문제가 해결되며, 성능 최적화에 큰 도움이 됩니다.

주의 사항

  • 연관된 엔티티의 개수에 주의해야 합니다: Fetch Join을 사용하면 한 번의 쿼리로 모든 연관 엔티티를 가져오기 때문에, 너무 많은 데이터가 조회될 경우 오히려 성능 저하를 일으킬 수 있습니다. 연관 엔티티의 수가 매우 많다면 신중하게 사용해야 합니다.

2. BatchSize

개념

  • BatchSize는 연관된 엔티티를 조회할 때, 한 번에 처리하는 데이터의 크기를 설정하는 방법입니다. JPA는 Lazy Loading을 사용할 때, 각각의 연관된 엔티티를 따로 조회합니다. 이 경우 많은 쿼리가 발생할 수 있는데, @BatchSize를 사용하면 연관된 엔티티를 일정한 배치(batch) 단위로 묶어서 한 번에 가져옵니다.

장점

  • 쿼리 수 감소: BatchSize를 설정하면 연관된 엔티티를 한 번에 일정 수만큼 묶어서 가져오기 때문에, 쿼리 수가 크게 감소합니다.
  • 메모리 절약: 한꺼번에 모든 연관 엔티티를 가져오는 Fetch Join과는 달리, BatchSize는 일정한 단위로 데이터를 묶어서 처리하기 때문에 메모리 사용량을 줄일 수 있습니다.

사용 방법

  • @BatchSize 어노테이션을 엔티티나 컬렉션 필드에 적용할 수 있습니다.
  • 예를 들어, Parent 엔티티가 여러 개의 Child 엔티티와 연관되어 있을 때, BatchSize를 적용하면 다음과 같습니다:
@Entity
public class Parent {
    @OneToMany
    @BatchSize(size = 10) // 한번에 10개의 자식 엔티티를 가져오도록 설정
    private List<Child> children;
}

  • 이 설정은 Parent 엔티티가 Lazy 로딩 방식으로 Child 엔티티를 조회할 때, 한 번에 최대 10개의 Child 엔티티를 묶어서 가져옵니다.

Global 설정 방법

  • @BatchSize는 개별 엔티티나 필드에 설정할 수도 있지만, 전체 애플리케이션에 대해 전역적으로 설정할 수도 있습니다. application.yml 혹은 application.properties에서 전역 BatchSize 설정을 할 수 있습니다:이 설정은 모든 엔티티에서 Lazy 로딩으로 연관된 엔티티를 가져올 때, 기본적으로 10개씩 묶어서 가져오도록 설정합니다.
  • spring: jpa: properties: hibernate: default_batch_fetch_size: 10

주의 사항

  • 적절한 배치 크기 설정: 배치 크기가 너무 크면 메모리 사용량이 늘어나고, 너무 작으면 쿼리 수가 많이 발생하여 성능 저하를 초래할 수 있습니다. 애플리케이션의 데이터 양과 성능을 고려하여 적절한 배치 크기를 설정하는 것이 중요합니다.

Fetch Join vs BatchSize

  • Fetch Join은 한 번의 쿼리로 연관된 엔티티를 모두 가져와야 하는 경우에 적합합니다. 단, 연관된 데이터가 많을 경우 성능 저하가 발생할 수 있습니다.
  • BatchSize는 Lazy 로딩과 함께 사용되며, 대량의 데이터를 한 번에 가져오는 대신 배치 단위로 나누어 처리함으로써 쿼리 수를 줄이고 메모리 사용을 최적화합니다.

이 두 가지 방법을 적절히 혼합하여 사용하면, JPA에서 발생하는 성능 문제를 효과적으로 해결할 수 있습니다냥!