Spring Data JPA - N+1 문제란?

몽루문·2024년 1월 19일
0

Spring Data JPA

목록 보기
6/7
post-thumbnail

📝 JPA의 N+1 문제란?

Spring Data JPA에서 N+1 문제를 알아보려고 한다.
N+1 문제는 하나의 엔티티를 조회할 때 연관된 엔티티를 조회하면서 불필요한 쿼리가 나가는 것이다.
그래서 N:1, 1:N 으로 묶여있는 엔티티에서만 발생하는데
이 문제를 어떠한 방식으로 해결할 수 있는지 알아보려고 한다.



🚩 N+1 문제가 발생하는 이유

  • JPA Repository로 find 시 실행하는 첫 쿼리에서 자식관계의 엔티티까지 한 번에 가져오지 않고, 자식관계 엔티티를 사용할 때 추가로 조회하기 때문이다.
  • JPQL은 기본적으로 글로벌 Fetch 전략을 무시하고 JPQL만 가지고 SQL을 생성하기 때문이다.


🚩 N+1 문제가 발생하는 시점

1. 즉시로딩 ( FetchType.EAGER )

  • 부모관계의 엔티티를 조회하는 즉시 발생한다.
  • 부모관계의 엔티티와 자식관계의 엔티티를 한번에 불러오는 옵션이기에 부모 엔티티를 조회하는 쿼리 한문장, 부모와 연관된 N개만큼 자식 엔티티를 조회하는 N개의 쿼리가 생성된다.
    따라서 1+N 개의 쿼리가 데이터베이스에 전달된다.

2. 지연로딩 ( FetchType.LAZY )

  • 부모관계의 엔티티를 조회할때는 발생하지 않지만, 부모관계의 엔티티에서 자식관계 엔티티의 데이터를 조회할 때 발생한다.
  • 부모관계의 엔티티를 불러오면서 부모관계 엔티티 안에 있는 자식관계의 엔티티 내용을 프록시로 채워지기에 당장에 N+1 문제가 발생하지 않지만, 자식관계 엔티티 데이터에 접근하는 순간 프록시 객체인 자식관계 엔티티의 데이터를 불러와야하기 때문에 즉시로딩과 마찬가지로 N+1 문제가 발생하게 된다.
→ 즉시로딩 ( FetchType.EAGER ), 지연로딩 ( FetchType.LAZY ) 모두 N+1문제가 발생하며, 어느시점에 발생되는지의 차이일 뿐이다.


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

1. Fetch Join

  • 부모관계의 엔티티를 조회할때 자식관계의 엔티티까지 SQL문의 JOIN을 이용해서 한번에 가져오게해주는 기능이다.
  • Code example
// JpaRepository.interface를 상속받아서 findAll Method를 Override한다.
@Override
@Query("select t from table_one t join fetch t.table_two_id")
List<Table> findAll();

■ 특징

  • 조회의 주체가 되는 부모 엔티티 이외에 Fetch join이 걸린 자식 엔티티도 같이 영속성 컨텍스트에서 관리해준다.
  • inner join이 발생한다.

■ 장점

  1. 단 한번의 쿼리만 발생하도록 설계할 수 있다.
  2. fetch join을 이용해 부모 엔티티의 자식 엔티티 모두를 가져올 수 있다.

■ 단점

  1. JPA의 최대 장점인 쿼리문 미사용인 점을 제대로 살릴 수 없으며, 계속해서 쿼리문을 작성해야한다.
  2. JPA가 제공하는 Pageable 기능 사용 불가하다. → 페이징 단위로 데이터 가져오기 불가능
    🔨 ( 해결법 ) : batch size로 해결한다. → 즉시로딩이나 지연로딩 시 자식 엔티티를 조회할 때 지정한 size 만큼 자식 엔티티의 PK를 SQL IN 절에 포함시킨다.
  • Code example
@Entity(name = "table_two")
public Class TableTwo {
  @BatchSize(size = 100)
  @ManyToOne
  private Table table;
}
  1. 1 : N 관계가 2개인 엔티티는 사용이 불가하다. → MultipleBagFetchException 발생

2. @Entitygraph

  • EntityGraph 상에 있는 연관관계 속에서 필요한 엔티티를 함께 조회하려고 할때 사용한다
  • Code example
// JpaRepository.interface를 상속받아서 findAll Method를 Override한다.
// attributePaths의 내용은 해당 Entity의 필드 명으로 한다.
@Override
@Entitygraph(attributePaths = {"table"})
List<Table> findAll();

■ 특징

  • attributePaths에 쿼리 수행 시 바로 가져올 필드명을 지정하면 지연로딩 ( FetchType.LAZY )이 아닌 즉시로딩 ( FetchType.EAGER ) 조회로 가져오게 된다.
  • outer join이 발생한다.

■ 장점

  1. fetch join의 매번 쿼리를 작성하고 확인하는 문제 해결된다.

■ 단점

  1. outer join을 사용하기 때문에 중복 데이터 발생한다. ( 카테시안 곱 현상 )
    🔨 ( 해결법 ) : DISTINCT / 필드타입을 Set으로 변경하여 해결한다.
  • Code example
// DISTINCT 해결법 1
@Override
@Query("select DISTINCT t from table_one t join fetch t.table_two_id")
List<Table> findAll();

// DISTINCT 해결법 2
@Override
@Entitygraph(attributePaths = {"table_two"})
@Query("select DISTINCT t from table_one t")
List<Table> findAll();

// 필드타입을 Set으로 변경하는 해결법
@Entity(name = "table_one")
public Class Table {
  @OneToMany
  private Set<TableTwo> table_two_list = new LinkedHashSet<>();
}


📌 마무리

Spring Data JPA에서 N+1 문제에 대해서 알아보았는데
@NamedEntitygraph 라는 기능도 있다고 한다. 어렵진 않은거 같은데 알아보면 여기에 추가로 적도록 하겠다.
이 문제에 대해서 깊게 생각해보지 않아서 잘몰랐는데 빈번히 발생하는 문제인거 같고, 파면 팔수록 어려운거 같다.

profile
알고 있는 것을 정리하고, 새로운 것을 알기위해 끄적이는곳..

0개의 댓글