-즉시 모든 것을 로딩하겠다는 의미에서 연관관계가 설정된 모든 테이블에 대해서 조인이 일어난다.
-데이터가 많은 테이블의 경우는 모든 데이터를 조인해서 가져오므로 성능 저하가 발생될 수 있다.
-연관관계에 설정에 대해서 추천하는 타입은 LAZY이며 EAGER는 1:1이라던지 한 건만 존재할 때 사용하는 것을 추천한다. (EAGER의 디폴트는 1:1)
-select *from item where id = ? 와 같이 선택한 id만 조회
-아래 연관관계를 위해 선언한 private List orderDetailList; 변수에 대해서 get 메서드를 호출하지 않는 이상 연관관계가 설정된 테이블에 대해서 select를 하지 않는다. (get 메서드를 통해 실제 엔티티가 사용될 때 DB에서 해당 엔티티를 조회해온다.)
연관관계에서 발생하는 이슈로 연관관계가 설정된 엔티티를 조회할 경우 조회된 데이터 갯수만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오게 된다.
FetchType을 변경하는 것은 단지 N+1 발생지점을 연관관계를 사용하는 시점으로 미룰지, 초기 데이터 로드 시점에 가져오느냐에 차이만 있다.
JpaRepository에 정의한 인터페이스의 메서드를 실행하면 jpa는 메서드를 분석해서 jpql을 생성해서 실행한다. jpql은 sql을 추상화한 객체지향 쿼리 언어로서 특정 sql에 종속되지 않고 엔티티 객체와 필드 이름을 가지고 쿼리한다. 만약 findAll() 메서드를 실행하면 해당 엔티티를 조회하는 쿼리만 실행하게 된다. (연관관계 데이터를 무시하고 해당 엔티티를 기준으로 조회한다.)
1) Fetch Join
원하는 코드를 jpql로 작성하는 것이다.
// inner join으로 호출됨
@Query("select a from User a join fetch a.id")
List<User> findAllJoinFetch();
이 방식의 단점은 Fetch Join 사용 시 데이터 호출 시점에 모든 연관관계의 데이터를 가지고 오기 때문에 FetchType을 Lazy로 하는 것도 의미가 없다. 또한, 하나의 쿼리로 가져오다 보니 jpa가 제공하는 페이징 api를 사용해서 페이징 단위로 데이터를 가져오는 것이 불가능하다.
2) EntityGraph
@EntityGraph의 attributePaths에 쿼리 수행 시 바로 가져올 필드명을 지정하면 Lazy가 아닌 Eager 조회로 가져오게 된다. Fetch Join과 동일하게 jpql을 사용해서 쿼리를 작성하고 필요한 연관관계를 @EntityGraph에 설정하면 된다.
// outer join으로 실행된다.
@EntityGraph(attributePaths = "id")
@Query(select a from user a)
List<User> findAllJoinFetch();
Fetch Join, EntityGraph는 공통적으로 카티시안 곱이 발생하여 User 수만큼 id가 중복 데이터가 존재할 수 있다.
-> 컬렉션 Set을 사용해서 중복을 허용하지 않거나 distinct를 사용해서 중복된 데이터를 조회하지 않도록 할 수 있다.
3) FetchMode.SUBSELECT
두 번의 쿼리로 해결하는 방법으로 해당 엔티티를 조회하는 쿼리는 그대로 발생하고 연관관계의 데이터를 조회할 때 서브쿼리로 함께 조회하는 방법이다.
@Fetch(FetchMode.SUBSELECT)
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<OrderDetail> orderDetailList;
즉시로딩이나 지연로딩을 원하는 시점에 설정하고 쿼리를 실행할 수 있다. 지연로딩으로 설정하고 성능 최적화가 필요한 곳에 jpql Fetch Join을 사용하는 것이 추천되는 방법이다.
4) BatchSize
Hibernate가 제공하는 org.hibernate.annotations.BatchSize 어노테이션을 이용하면 연관된 엔티티를 조회할 때 지정된 size만큼 sql의 in절을 사용해서 조회한다.
@BatchSize(size=10)
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<OrderDetail> orderDetailList;
user의 row 갯수만큼 추가 sql을 날리지 않고, 조회한 User의 id들을 모아서 sql in절을 날린다. size는 in절에 올 수 있는 최대 인자 갯수로 만약 id가 100개라면 in절이 10번 실행된다.
5) QueryBuilder (QueryDSL, Mybatis, JOOQ, JDBC Template 등..)
Query를 실행하도록 도와주는 플러그인을 사용하면 최적의 쿼리를 구현할 수 있다.
// QueryDSL
return from(user).leftJoin(user.id, id)
.fetchJoin()
*참고 자료
https://jojoldu.tistory.com/165
https://incheol-jung.gitbook.io/docs/q-and-a/spring/n+1