N + 1 문제란?
- 연관 관계에서 발생하는 이슈로 연관 관계가 설정된 엔티티를 조회할 경우 조회된 데이터 갯수(n) 만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오게 되는 문제.
왜 발생하는 것인가?
- jpaRepository에 정의한 인터페이스 메서드를 실행하면 JPA는 메서드 이름을 분석하여 JPQL을 생성하여 실행하게된다.
- JPQL은 SQL을 추상화한 객체지향 쿼리 언어로서 특정 SQL에 종속되지 않고 엔티티 객체와 필드 이름을 가지고 쿼리를 한다.
- 그렇기 때문에 JPQL은 findAll()이란 메서드를 수행하였을 때 해당 엔티티를 조회하는 select * from table 쿼리만 실행하게 된다.
- JPQL 입장에서는 연관관계 데이터를 무시하고 해당 엔티티 기준으로 쿼리를 조회
- 그렇기 때문에 연관된 엔티티 데이터가 필요한 경우, FetchType으로 지정한 시점에 별도로 호출하게 된다.
해결방안
Fetch join
- 사용자가 원하는것은 join 코드 일 것
- 최적화된 쿼리를 우리가 직접 사용할 수 있음 → Fetch join
- JpaRepository에서 제공해주는 것은 아니고 JPQL로 작성해야 한다.
- ex)
@Query("select o from Owner o join fetch o.cats")
List<Owner> findAllJoinFetch();
- 연관관계의 연관관계가 있을 경우에도 하나의 쿼리 문으로 표현할 수 있으므로 유리하다.
- 데이터 호출 시점에 모든 연관 관계의 데이터를 가져오기 때문에 FetchType을 Lazy로 해놓는 것이 무의미해짐
EntityGraph
- @EntityGraph의 attributePaths에 쿼리 수행시 바로 가져올 필드명을 지정하면 Lazy가 아닌 Eager 조회로 가져오게 된다.
- Fetch join과 동일하게 JPQL을 사용하여 query문을 작성하고 필요한 연관관계를 EntityGraph에 설정하면 된다.
- Fetch join과는 다르게 join문이 outer join으로 실행된다. ( Fetch join == inner join)
- ex
@EntityGraph(attributePaths = "cats")
@Query("select o from Owner o")
List<Owner> findAllEntityGraph();
컬렉션 Fetch join
- 컬렉션 fetch join은 일대다 관계에서 사용할 수 있으며 데이터가 많아질 수 있다.
- DISTINCT로 중복 제거가 가능
- SQL의 DISTINCT
- JPQL의 DISTINCT
- SQL의 DISTINCT 기능 뿐만 아니라 동일한 엔티티면 중복 제거
- application단에서 엔티티 중복을 제거한다.
Fetch join의 한계와 극복
- 컬렉션을 fetch join 하면 페이징 API를 사용할 수 없다.
- 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인을 해도 페이징이 가능하다.
- HIbernate는 경고로그를 남기며 메모리에서 페이징을 한다.
- 전체 데이터를 가져와 메모리에 올려놓고, 메모리에서 N개씩 데이터를 결과로 보여준다. 자칫 OutOfMemory가 발생할 수 있고, 성능에 영향을 줌
xxxToOne 관계를 모두 페치조인 한다.
- toOne관계는 row수를 증가시키지 않기 때문
- 컬렉션은 지연로딩으로 조회
@컬렉션은 지연로딩으로 설정 → Batch_fecth_size를 사용
- hibernate.default.batch_fetch_size(글로벌 설정)
- @BatchSize(디테일 설정)
- 프록시 객체를 설정한 size만큼 in 쿼리로 조