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(즉시 로딩)인 경우
- JPQL에서 만든 SQL을 통해 데이터를 조회한다
- 이후 JPA에서 Fetch 전략을 가지고 해당 데이터의 연관 관계인 하위 엔티티들을 추가로 조회한다
- 2번의 과정으로 N+1 문제가 발생한다
LAZY(지연 로딩)인 경우
- JPQL에서 만든 SQL을 통해 데이터를 조회한다
- JPA에서 Fetch 전략을 가지지만, 지연 로딩이기 때문에 추가 조회하지 않는다
- 하지만, 하위 엔티티를 가지고 작업하게 될 경우 추가 조회가 발생하게 되어 결국 N+1문제가 발생한다
* 지연로딩은 프록시 객체를 로딩하는 과정 중 하나로, 연관된 객체를 "사용"하는 시점에 로딩을 해주는 방법이다.
연결된 Entity에 대해 프록시를 걸고, "사용"할 때 결국 쿼리문을 발생시키기 때문에 프록시에 대한 쿼리가 발생하게 되는 것이다.
N+1 문제 해결책
해결 방법에는 여러 방법이 있지만, FetchJoin과 EntityGraph 방법에 대해 살펴보자.
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을 사용하자
'JVM > JPA' 카테고리의 다른 글
[JPA] QueryDSL 소개 및 프로젝트 설정하기 (0) | 2023.05.25 |
---|---|
[JPA] 기본 키 전략과 Entity Custom ID Generator 구현하기 (0) | 2022.08.05 |
[JPA] SpringBoot JPA 쿼리 로그 설정하기 (0) | 2022.07.27 |
[JPA] JpaRepository를 상속 받은 인터페이스는 @Repository 없이 어떻게 인스턴스화될까? (0) | 2022.07.26 |
댓글