반응형
1. N+1 문제란?
- 예를 들면 주문(Orders)과 주문정보(Order_item) 2개의 1:N의 구조의 엔티티가 있다고 가정하자.
주문과 주문정보를 전체 데이터를 조회할때 일반적으로 join문을 사용하여 한번의 쿼리를 통해 데이터를 조회할 수 있다. - 그러나 JPA에서는 1번 조회되어야 할게 각각 N번을 추가로 조회하게 되어 총 N+1을 DB에서 조회하게 되는 문제가 발생하게 되는데 이러한 문제를 'N+1 문제'라고 말합니다.
- N+1 문제는 N:1 , 1:N을 가진 엔티티를 조회할 때 발생하게 됩니다.
2. N+1 문제가 발생하는 경우 - EAGER Loading
- Fetch type을 EAGER 전략으로 하였을 경우에 N+1문제가 발생한다.
예를 통해 설명을 하면 다음과 같다.
주문(orders) 엔티티와 주문아이템(order_item) 엔티티는 1:N의 관계이고 주문(order)에는 총 2개의 데이터가 존재한다.
fetch 전략을 EAGER로 설정된 상태이다. - 전체 주문 내역을 조회하는 findAll() 메소드를 호출하게 되면 주문정보에 대한 DB 조회와 주문아이템 정보에 대한 DB 조회 2번 , 총 3번의 DB 조회가 발생하게 된다.
- DB 조회가 3번 발생하는 이유는 다음과 같다.
주문 테이블에는 총 2개의 주문 데이터가 존재하기 때문에 그 하위 엔티티인 Order_item에 대해서도 order_id가 4, 12인 데이터를 2번 더 조회하게 된다. 그렇기 때문에 실제로는 findAll()을 통해 전체 주문 정보만 조회하였으나 추가로 주문 아이템 정보를2번 더 조회하면서 총 3번의 DB 조회가 발생하게 된다.
3. N+1 문제가 발생하는 경우 - LAZY Loading
- Fetch type이 LAZY Loading의 경우에도 N+1 문제가 발생할 수 있다.
예를 들어 설명하면 다음과 같다.
위의 EAGER 전략에서 사용했던 것과 동일하게 주문(orders) 엔티티와 주문아이템(order_item) 엔티티는 1:N의 관계이고 주문(order)에는 총 2개의 데이터가 존재한다.
fetch 전략의 경우 LAZY로 설정된 상태이다. - 전체 주문 내역을 조회하는 findAll() 메소드를 호출하게 되면 EAGER 와는 다르게 하위 엔티티에 접근하지 않기 때문에 주문 정보에 대한 DB 조회만 발생하게 된다.
- 그러나 이후 하위 엔티티를 조회하는 경우가 발생하게 되면 그 하위 엔티티인 order_item에 대해서 order_id가 4, 12인 데이터를 2번 더 조회하게 된다.
결과적으로 EAGER Loading과 동일하게 N+1 문제가 발생하게 된다.
4. 해결 방법
- Fetch Join(패치 조인) 사용
- Fetch Join은 JPQL에서 성능 개선 및 최적화를 위해 제공하는 기능으로 연관 엔티티와 컬렉션을 한번에 조회하는 역할을 한다.
- 사용하는 방법은 JPQL 쿼리에서 join 명령어 마지막에 fetch를 넣어주면 된다.
- Fetch Join을 실행하게 되면 아래와 같이 엔티티간에 inner join문이 작성되어 SQL이 실행된다.
주문정보, 주문 아이템 정보 모두 프록시가 아닌 실제 엔티티로서 한꺼번에 조회되면서 N+1 문제가 해결된다. - 주의할 점은 1:N 조인의 경우 중복 데이터가 발생할 수 있으므로 DISTINCT를 이용해 중복을 제거하는 것이 좋다.
- 일반조인과 패치 조인의 차이는 다음과 같다. 일반 조인의 경우 실제 쿼리에 join을 걸지만 join 대상에 대한 영속성까지는 관연하지 않습니다.
간단히 설명하면 SELECT 절에 지정한 엔티티만 조회하여 조회되기 때문에 연관된 엔티티는 조회되지 않는다. 그렇기에 LAZY LOADING에서 발생한 것과 동일하게 N+1 문제가 발생한다. - 패치조인의 단점
- JPA가 제공한느 Pageable 기능 사용불가(페이징 API)
- 1:N 관계 컬렉션이 2개인 엔티티의 경우 패치 조인 사용 불가
- @EntityGraph
- 엔티티 그래프란?
엔티티 그래프는 엔티티매니저에게 전달하는 쿼리힌트라고 보면 된다. - 사용하는 방법은 Repository.class에서 사용하고자 하는 메소드에 @EntityGraph annotation을 붙여 사용한다. 옵션 attributePaths에는 연관된 엔티티명을 적으면 되고, 콤마를 통해 여러개도 사용 가능하다.
기본적으로 엔티티를 정의할 때 -ONE으로 끝나는 연관관계는 기본값이 EAGER이고 -MANY로 끝나는 연관관계는 기본값이 LAZY이다.
@EntityGraph로 정의 시 EAGER모드가 적용되며 페치 조인과는 다르게 Outer 조인으로 쿼리를 생성한다. - 추가적으로 Type을 지정할수가 있다. Type에는 EntityGraphType.LOAD , EntityGraphType.FETCH 2가지가 있다.
- LOAD : attributePaths에 정의한 엔티티들은 EAGER, 나머지는 글로벌 페치 전략에 따라 페치전략을 불러온다 - FETCH : attributePaths에 정의한 엔티티들은 EAGER, 나머지 엔티티는 LAZY 로 불러온다.
- 엔티티 그래프란?
- @BatchSize
- @BatchSize는 하위 엔티티들에 대해 설정된 size만큼 쿼리문에 in절 조건이 생성되어 DB를 조회하게 된다. 이렇게하면 IN 조건절이 실행되기 때문에 동일한 쿼리를 연속해서 발생하지 않는다는 장점이 있다.
- 또 다른 방법으로는 hibernate.default_batch_fetch_size라는 옵션이 있습니다. 이 옵션을 적용 시 @BatchSize와 동일하게 지정한 숫자만큼 in Query로 로딩한다.
- @FetchMode.SUBSELECT
- @BatchSize와 유사하게 In Query로 로딩하지만 가장 큰 차이점은 Batchsize는 정해준 크기만큼 In절 조건이 발생하지만 FetchMode의 경우는 하위엔티티 조회시 엔티티 전체 데이터를 In절 조건으로 셋팅되어 조회하게 됩니다.
반응형
'Backend > Spring' 카테고리의 다른 글
Spring Data JPA -Cascade와 orphanRemoval? (0) | 2022.03.03 |
---|---|
Spring OSIV? (0) | 2022.03.02 |
JPA 즉시로딩(Eager Loading)과 지연로딩(Lazy Loading)? (0) | 2022.02.24 |
JPA 프록시란? (0) | 2022.02.24 |
영속성 컨텍스트란? (0) | 2022.02.22 |