JPA N+1

Terror·2024년 9월 27일
0

JPA N+1 문제란

  • 1번의 쿼리를 날렸을때 의도하지 않은 N번의 쿼리가 추가적으로 실행되는 것을 의미

언제 발생하는가 ?

  • JPA Repository를 활용하여 인터페이스 메서드 호출시 (Read)

누가 발생시키는가 ?

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

어떤 상황에 발생되는가 ?

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

왜 발생하는가 ?

  • JPA Repository로 find 실행하는 쿼리에서 하위 엔티티까지 "한번에 가져오지 않고" 하위 엔티티를 사용할때 추가로 조회하기 때문에
  • JPQL은 기본적으로 글로벌 Fetch 전략을 무시하고 (JQPL내에서 어떻게 가져올지 명시했다면 Entity에 설정한 Fetch전략을 무시한다는뜻) , JPQL만 가지고 SQL을 생성하기 때문에

EAGER

  • JPQL에서 만든 SQL을 통해 데이터(1) 조회
  • 이후 JPA에서 EAGER 전략을 가지고 해당 데이터의 연관 관계인 하위 엔티티들(N)을 추가 조회
  • 예를들어 10개의 게시글이 있고 각각 10개의 댓글들이 있다고 전제 하겠다
  • 1번째 게시글 조회 (10번의 댓글 select 쿼리문 날라감), 2번째 게시글 조회(10번의 댓글 select 쿼리문 날라감) 과 같은 문제가 발생된다

LAZY

  • JQPL에서 만든 SQL을 통해 데이터를 조회
  • JPA에서 Fetch 전략을 가지지만, 지연 로딩이기떄문에 추가 조회를 하지않음(즉 댓글들이 필요전까지는 가져오지않음)
  • 하지만, 추후에 게시글에 관련된 댓글들을 가져와야할때는 어쩔수없이 다시 N+1문제가 발생됨

실습 - 깃헙에 올려둠

https://github.com/kkm4512/fetch-lazy-test

문제발생

  • N+1문제가 발생되는것을 볼 수 있다

문제발생 (상세히)

  • 1개의 onwer를 가져옴
  • 그 1개에 해당하는 pet들을 가져옴

N+1 해결 방법

  • 해결방법에는 여러가지가 있지만, 이번에는 FetchJoin과 EntityGraph 두가지 방법을 알아보자

FetchJoin

  • N+1 문제가 발생하는 이유는 한쪽 테이블만 조회하고, 연결된 다른 테이블은 "따로" 조회 하기 때문이다
  • 미리 두 테이블을 Join하여 한번에 데이터를 가져올 수 있다면 애초에 N+1 문제가 발생되지 않을 것이다
  • 그렇데 나온 방법이 FetchJoin이며, 이는 JpaRepositry에서 @Query를 적용하여 사용가능하다
public interface OwnerRepository extends JpaRepository<Owner, Long> {
    @Query("SELECT o from Owner o JOIN FETCH o.pets")
    List<Owner> findAllJoinFetch();
}

  • 편-안 한 모습을 볼 수 있다

FethJoin 단점

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

@Entity Graph

  • @EntityGraph의 attributePahts는 같이 조회할 연관 엔티티명을 적으면 된다
  • ,(콤마)를 통해 여러개 줄 수도 있다
  • Fetch Join과 동일하게 JPQL을 사용하여, Query문을 작성하고 필요한 연관관계를 EntityGraph에 설정하면된다
public interface OwnerRepository extends JpaRepository<Owner, Long> {
    @EntityGraph(attributePaths = {"pets"})
    @Query("SELECT DISTINCT o from Owner o")
    List<Owner> findAllJoinFetch();
}

  • 잘 나오는 모습이다

  • Fetch Join과 @EntityGraph의 출력되는 결과를 보면 알다시피
    Fetch Join = innter join, EntityGraph = outer join
    을 기본으로 한다

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

Fetch Join과, EntiyGraph 사용시 주의점

  • FetchJoin과 EntityGraph는 공통적으로 카테시안 곱 (Cartesian Product)가 발생하여 중복이 생길 수 있다

    카테시안곱: 두 테이블 사이에 유효 Join 조건을 적지 않았을때 해당 테이블에 대한 모든 데이터를 전부 결합하여 테이블에 존재하는 행 갯수를 곱한만큼의 결과값이 (즉 Join할게 없으니까 가능한 모든 경우의수를 가져오게하는것)

  • 그래서 Select할떄 DESTINT를 넣어준것이다

TMI

  1. Lazy는 필요한 정보만 쿼리해서 가져온다 (하위 엔티티 객체의 데이터들을 담아주지 않는다)
  2. Join은 데이터를 Join해서 가져온다 (하지만 이것도 하위 엔티티 객체의 데이터들을 담아주지 않는다)
  3. JoinFetch 데이터를 Join해서 가져온다 (하위 에닡티 객체의 데이터들을 담아준다 = N+1 문제 해결가능)

참조블로그

https://dev-coco.tistory.com/165

profile
테러대응전문가

0개의 댓글