JPA를 사용하게되면 자주만나게 되는 문제가 N+1 문제다. 막연하게 @OneToMany
는 batchSize로 해결하고, @ManyToOne
은 fetchType을 Lazy로 변경하고 fetchJoin을 통해서 해결한다고 알고 있었다. N+1을 더 자세히 알아보고 다양한 해결방법도 다뤄보고자 한다.
팀과 멤버가 서로 양방향 연관관계를 갖고 있다. 모든 Team을 조회해서 Member들의 목록을 받아왔을 때 쿼리가 몇번 나갈까?
팀을 조회하고 팀에 속해있는 회원을 조회하기 위해서 총 10번의 추가쿼리가 발생한다.
fetch join을 통해서 해결할 수 있다.
/**
* 1.joinFetch로 해결한다.
*/
@Query("select t from Team t join fetch t.members")
List<Team> findAllJoinFetch();
조회시 같이 가져와야할 엔티티를 지정하는 것입니다.
해당 fetch join문을 이용해 값을 가져오면 10개의 쿼리가 사라지고 1개로 줄어든다.
만약 member 하위의 entity까지 한번에 가져와야 할때도 쿼리문 수정을 통해서 해결할 수 있다.
Team의 BatchSize를 조정해서 해결할 수도 있다. 하이버네이트가 제공하는 BatchSize 어노테이션을 사용하면 연관된 엔티티를 조회할 때 지정한 size만큼 in절을 사용해서 조회한다.
batchsize가 4이고 조회해야하는 데이터가 10개일 경우 in 절을 사용해서 4개를 조회하는 쿼리가 2번 나가고, 2개를 조회하는 쿼리가 한번 나간다.
엔티티에 @OneToMany
가 2개 이상 존재할 때 fetchJoin을 선언하는 경우 MultipleBagFetchException
이 발생한다.
애초에 ApplicationContext를 로딩하는 시점에 쿼리문을 조회해서 컴파일 에러가 발생한다.
이를 방지하기 위해서 @OneToMany
가 여러개일 경우 하나만 fetch join을 걸고 나머지는 batchsize로 해결할 수 있다.
@EntityGraph(attributePaths = "members")
@Query("select t from Team t")
List<Team> findAllEntityGraph();
attributePaths에 쿼리 수행시 바로 가져올 필드명을 지정하면 Eager로 가져온다. 원본 쿼리에 관계없이 필드값을 가져올 수 있다.
@OneToMany
2개를 가져오면 MultipleBagFetchException
이 발생한다.
예시를 위해서 팀별로 멤버를 2명씩 넣어보자.
Fetch Join은 InnerJoin을 EntityGraph는 Outer Join을 한다. 공통적으로 member의 수만큼 team이 중복 발생하게 된다고 생각했는데 Fetch Join만 카테시안 곱만큼의 데이터가 발생한다. EntityGraph에서는 데이터가 갯수에 맞춰서 가져온다.
따라서 Distinct 혹은 Team을 Set으로 설정하여 해결할 수 있다.
@ManyToOne
은 fetchJoin으로 해결하자
@OneToMany
일 경우 batchSize 혹은 EntityGraph로 해결하자.
fetchJoin을 할 경우 카테시안 곱이 발생할 수 있으니 List가 아닌 Set으로 하던가 Distinct 쿼리를 보내자
@EntityGraph
로는 카테시안 곱이 발생하지 않는다. 왜인지는 아직 탐구중이다.
자바 ORM 표준 JPA 프로그래밍 - 김영한 지음
https://jojoldu.tistory.com/165
https://jojoldu.tistory.com/457
감사합니다 👍