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
문제발생
문제발생 (상세히)
- 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
- Lazy는 필요한 정보만 쿼리해서 가져온다 (하위 엔티티 객체의 데이터들을 담아주지 않는다)
- Join은 데이터를 Join해서 가져온다 (하지만 이것도 하위 엔티티 객체의 데이터들을 담아주지 않는다)
- JoinFetch 데이터를 Join해서 가져온다 (하위 에닡티 객체의 데이터들을 담아준다 = N+1 문제 해결가능)
참조블로그
https://dev-coco.tistory.com/165