[JPA] N+1 문제

개발의 숩
|2023. 3. 19. 21:07

JPA를 사용하다 보면 의도하지 않았지만 여러 번의 select문이 여러 개가 나가는 현상을 본 적이 있을 것이다.

이러한 현상을 N+1문제라고 부른다.

 

 

N+1 문제

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

지연로딩에서는 N+1문제가 발생하지 않는 것처럼 보이지만, 막상 객체를 탐색하려고 하면 N+1문제가 발생되어 즉시로딩과 N+1문제가 발생되는 시점만 다를 뿐이다.

 

 

N+1 발생 원인

JPA가 JPQL을 분석해서 SQL을 생성할 때는 글로벌 Fetch 전략을 참고하지 않고, 오직 JPQL 자체만을 사용한다

즉, 아래와 같은 순서로 동작한다.

 

 

해결 방안

Fetch Join, EntityGraph 어노테이션, Batch Size등의 방법이 있다.

 

 

1. Fetch join

JPQL을 사용하여 DB에서 데이터를 가져올 때 처음부터 연관된 데이터까지 같이 가져오게 하는 방법이다. SQL Join문을 생각하면 될 것 같다.

별도의 메소드를 만들어줘야 하며 @Query 어노테이션을 사용해서 “join fetch 엔티티.연관관계_엔티티” 구문을 만들어 주면 된다.

SQL 로그를 보면 별도의 지정을 안하면 JPQL에서 join fetch 구문은 SQL문의 inner join구문으로 변경되어 실행된다.

 

 

Fetch Join의 단점

Fetch join도 유용해보이지만, 단점은 있다.

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

 

 

 

2. EntityGraph 어노테이션

@EntityGraph 라는 어노테이션을 사용해서 fetch 조인을 하는 것인데, 사용하는 순간 관계가 조금만 복잡해도 수습하기가 어렵다고 한다 이런 게 있는 구나 하고 알기만 하고 사용하지 말자

Fetch join과 는 다르게 join문이 outer join으로 실행된다.

 

 

 

3. Batch Size

정확히는 N+1 문제를 안 일어나게 하는 방법이 아니라 N+1문제가 발생하더라도 select * from user where team_id =? 이 아닌 SQL의 IN절을 사용하는 방식으로 N+1문제가 발생하게 하는 방법이다. 이렇게 하면 100번 일어날 N+1 문제를 1번만 더 조회하는 방식으로 성능 최적화를 할 수 있다.

application.yml

jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        **default_batch_fetch_size: 1000 #최적화 옵션**

 

 

4. QueryBuilder

Query를 실행하도록 지원해주는 다양한 플러그인이 있다

대표적으로 Mybatis, QueryDSL, JOOQ, JDBC Template 등이 있을 것이다

이를 사용하면 로직에 최적화된 쿼리를 구현할 수 있다.

// QueryDSL로 구현한 예제
return from(owner).leftJoin(owner.cats, cat)
                   .fetchJoin()

 

 

Fetch Join과 EntityGraph 주의할 점

Fetch join과 EntityGraph는 JPQL을 사용하여 Join문을 호출한다는 공통점이 있다.

또한, 공통적으로 카테시안 곱(Cartesian Product)이 발생하여 중복데이터가 존재할 수 있다. 그러므로 중복된 데이터가 컬레션에 존재하지 않도록 주의해야 한다

그렇다면 어떻게 중복된 데이터를 제거할 수 있을까

  • 컬렉션을 Set을 사용하게 되면 중복을 허용하지 않는 자료구조이기때문에 중복된 데이터를 제거할 수 있다.
  • JPQL을 사용하기 때문에 distinct을 사용하여 중복된 데이터를 조회하지 않을 수 있다.

 

 

정리

N+1 은 연관관계를 맺는 엔티티를 사용한다면 한 번쯤 부딪힐 수 있는 문제

상황에 따라 어떻게 해결할지 충분히 고민끝에 골라서 성능 최적화를 시켜야 하는 부분이다

마냥 N+1을 해결하기 위해 fetch join을 한다고 해서 추후 페이징을 못하는 경우도 있기때문이다.

 

'TIL' 카테고리의 다른 글

[JPA] API 개발 고급 정리  (1) 2023.03.21
[JPA] 페이징과 한계돌파  (0) 2023.03.20
[JPA] respository에 DTO 사용 ?  (0) 2023.03.18
[JPA] DTO 설계 이유  (0) 2023.03.18
[JPA] Entitiy 대신 ResponseDto,RequestDto 사용, 그 이유  (0) 2023.03.17