JVM/JPA

[JPA] N+1 발생 원인과 해결책

헹창 2023. 5. 30.
반응형

JPA N+1문제


1번의 쿼리를 조회하기 위해 설계하였으나, 의도하지 않은 N번의 쿼리가 추가적으로 실행되는 문제

 

When . 언제 발생하는가 ?

  • JPA Repository를 활용해 find 인터페이스 메소드를 호출할 때 발생

Who . 누가 발생시키는가 ?

  • 1:N 또는 N:1 관계를 가진 엔티티 조회 시 발생

How . 어떻게 발생되는가 ?

  • JPA Fetch 전략이 EAGER 전략으로 데이터 조회하는 경우
  • JPA Fetch 전략이 LAZY 전략으로 데이터를 가져온 이후, 연관 관계인 하위 엔티티를 다시 조회하는 경우

Why . 왜 발생하는가 ?

  • JPA Repository find 메소드 실행 시 첫 쿼리에서 하위 엔티티까지 한 번에 가져오지 않고, 하위 엔티티를 사용할 때 추가로 조회하기 때문에 발생
  • JPQL은 기본적으로 글로벌 Fetch 전략을 무시하고 JPQL만 가지고 SQL을 생성하기 때문에 발생

 

EAGER(즉시 로딩)인 경우

  1. JPQL에서 만든 SQL을 통해 데이터를 조회한다
  2. 이후 JPA에서 Fetch 전략을 가지고 해당 데이터의 연관 관계인 하위 엔티티들을 추가로 조회한다
  3. 2번의 과정으로 N+1 문제가 발생한다

LAZY(지연 로딩)인 경우

  1. JPQL에서 만든 SQL을 통해 데이터를 조회한다
  2. JPA에서 Fetch 전략을 가지지만, 지연 로딩이기 때문에 추가 조회하지 않는다
  3. 하지만, 하위 엔티티를 가지고 작업하게 될 경우 추가 조회가 발생하게 되어 결국 N+1문제가 발생한다

* 지연로딩은 프록시 객체를 로딩하는 과정 중 하나로, 연관된 객체를 "사용"하는 시점에 로딩을 해주는 방법이다.

연결된 Entity에 대해 프록시를 걸고, "사용"할 때 결국 쿼리문을 발생시키기 때문에 프록시에 대한 쿼리가 발생하게 되는 것이다.

 

N+1 문제 해결책


해결 방법에는 여러 방법이 있지만, FetchJoinEntityGraph 방법에 대해 살펴보자.


Fetch Join (패치 조인) 사용 :Inner Join 

N+1 이 발생하는 근본적인 원인은 한쪽 테이블만 조회를 하고 연결된 테이블을 각각 따로 조회하기 때문이다

미리 두 테이블을 JOIN 하여 한 번에 모든 테이블을 조회한다면 N+1 문제가 발생하지 않을 것이다

 

패치 조인이란, JPQL 에서 성능 개선 및 최적화를 위해 제공하는 기능으로, 연관된 엔티티나 컬렉션을 한 번에 조회하는 역할을 한다. 이는 SQL 쿼리를 통해 객체 그래프를 한 번에 조회할 수 있다.

public interface TeamRepository extends JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
	@Query("select distinct t from Team t join fetch t.members")
	List<Team> findAllWithJoinFetch();
}

Fetch Join  (패치 조인) 단점

  • 쿼리 한 번에 모든 데이터를 가져오기 때문에 JPA가 제공하는 Pageable 사용이 불가능
  • 1:N 관계가 두 개 이상인 경우 사용 불가능
    MultipleBagFetchException 발생
  • 패치 조인 대상에게 별칭(as) 부여 불가능
  • 번거롭게 쿼리문 작성 필요

 

EntityGraph 사용 : Outer Join

@EntityGraph 의 attributePaths는 같이 조회할 연관 엔티티명을 적어서 사용하며 콤마(,)를 통해 여러 개를 설정가능

Fetch Join은 inner join을 하는 반면, Entity Graph는 outer join을 기본으로 한다.

* 기본적으로 outer join 보다 inner join이 성능 최적화에 유리하다

public interface TeamRepository extends JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
	@EntityGraph(attributePaths = {"members"})
	List<Team> findAll();
}

EntityGraph의 경우에는 SpringJPA 기본적으로 제공하는 메소드에 활용가능하여, Pageable 등에도 설정가능하다

public interface TeamRepository extends JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
	@Orverride
	@EntityGraph(attributePaths = {"members"})
	Page<Team> findAll(@Nullable Specification<T> spec, Pageable pageable);
}

 

 

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다"

추천인 코드 : AF8800551

 

 

Fetch Join과 EntityGraph 사용 시 주의점


FetchJoin과 EntityGraph는 공통적으로 카테시안 곱(Cartesian Product)가 발생하여 중복 데이터를 조회할 수 있다.

* 카테시안 곱 : 두 테이블 사이에 유효 조인 조건을 적지 않을 경우 해당 테이블에 대한 모든 데이터를 전부 결합하여 테이블에 존재하는 행 갯수를 곱한만큼의 결과 값이 반환되는 것

 

해결 방안

  • Fetch Join (패치 조인) 시 DISTINCT를 추가하여 중복 제거
  • OneToMany 필드 타입을 Set으로 선언하여 중복 제거

* Set은 순서가 보장되지 않은 특징이 있기 때문에, 순서 보장이 필요한 경우 LinkedHashSet을 사용하자

 

 

728x90
반응형

댓글

추천 글