JPA N+1

qkrrnjswo·2023년 8월 1일
0

공부 정리

목록 보기
23/24

N+1?

연관 관계에서 발생하는 이슈로
연관 관계가 설정엔티티를 조회하는 경우에
조회된 데이터 갯수(n) 만큼 연관관계의 조회 쿼리가 추가로 발생하여
데이터를 읽어오는 현상

발생이유

N+1 문제가 발생하는 이유는 JPA가 JPQL을 분석해서 SQL을 생성할 때는 글로벌 Fetch 전략을 참고하지 않고 오직 JPQL 자체만을 사용한다.

Fetch 전략이 즉시 로딩인 경우

  1. findAll()을 한 순간 select t from Team t 이라는 JPQL 구문이 생성되고 해당 구문을 분석한 select * from team 이라는 SQL이 생성되어 실행된다. ( SQL 로그 중 Hibernate: select team0.id as id1_0, team0.name as name2_0 from team team0_ 부분 )
  2. DB의 결과를 받아 team 엔티티의 인스턴스들을 생성한다.
  3. team과 연관되어 있는 user 도 로딩을 해야 한다.
  4. 영속성 컨텍스트에서 연관된 user가 있는지 확인한다.
  5. 영속성 컨텍스트에 없다면 2에서 만들어진 team 인스턴스들 개수에 맞게 select * from user where team_id = ? 이라는 SQL 구문이 생성된다. ( N+1 발생 )

Fetch 전략이 지연 로딩인 경우

  1. findAll()을 한 순간 select t from Team t 이라는 JPQL 구문이 생성되고 해당 구문을 분석한 select * from team 이라는 SQL이 생성되어 실행된다. ( SQL 로그 중 Hibernate: select team0.id as id1_0, team0.name as name2_0 from team team0_ 부분 )
  2. DB의 결과를 받아 team 엔티티의 인스턴스들을 생성한다.
  3. 코드 중에서 team 의 user 객체를 사용하려고 하는 시점에 영속성 컨텍스트에서 연관된 user가 있는지 확인한다
  4. 영속성 컨텍스트에 없다면 2에서 만들어진 team 인스턴스들 개수에 맞게 select * from user where team_id = ? 이라는 SQL 구문이 생성된다. ( N+1 발생 )

해결방안

Fetch join

원하는 쿼리는 select * from () left join () on ().()_id = ().id Fetch join을 사용하면 최적화된 쿼리를 직접 사용할 수 있다.
하지만 jpaRepository에서 제공해주는 기능이 아니므로 JPQL로 직접 작성해야 한다.

Fetch Join의 단점은 우리가 연관관계 설정해놓은 FetchType을 사용할 수 없다는 것이다.
Fetch Join을 사용하게 되면 데이터 호출 시점에 모든 연관 관계의 데이터를 가져오기 때문에 FetchType을 Lazy로 해놓는것이 무의미하다.
하나의 쿼리문으로 가져오다 보니 페이징이 불가능하다.

EntityGraph

@EntityGraph 의 attributePaths에 쿼리 수행시 바로 가져올 필드명을 지정하면 Lazy가 아닌 Eager 조회로 가져오게 된다.
사용하지 않는 것을 권장.

@EntityGraph(attributePaths = "cats")
@Query("select o from Owner o")
List<Owner> findAllEntityGraph();

BatchSize

하이버네이트가 제공하는 org.hibernate.annotations.BatchSize 어노테이션을 이용하면 연관된 엔티티를 조회할 때 지정된 size 만큼 SQL의 IN절을 사용해서 조회한다.
즉시로딩이므로 Owner를 조회하는 시점에 Cat를 같이 조회한다.
@BatchSize가 있으므로 Cat의 row 갯수만큼 추가 SQL을 날리지 않고, 조회한 Owner 의 id들을 모아서 SQL IN 절을 날린다.

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Owner {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String name;

    @BatchSize(size=5)
    @OneToMany(mappedBy = "owner", fetch = FetchType.EAGER)
    private Set<Cat> cats = new LinkedHashSet<>();
}

참고

https://programmer93.tistory.com/83
https://incheol-jung.gitbook.io/docs/q-and-a/spring/n+1

0개의 댓글