Spring Data JPA (Hibernate), N + 1 Query Problem

N + 1 문제가 일어나는 모든 경우의 수에 대해 알아보고..

한번 정리를 해보겠다.

 

 

N + 1 Query problem ?

어떤 엔티티를 조회하기 위해 쿼리(1)를 수행했는데

예상치 못한 쿼리들(N)이 추가로 수행되는 상황을 말한다.

주로 일대다, 다대일 관계에서 발생한다.

 

이번 포스팅에서 사용할 DB 다이어그램이다.

Team 이 여러 Member 를 가지는 관계이다.

 

 

N + 1 조회 문제가 일어나는 케이스를 살펴보겠다.

 

글로벌 페치 전략이 즉시 로딩(Eager loading) 인 경우

JPQL 을 이용하여 어떤 엔티티를 조회 할 경우 발생한다.

-> 글로벌 페치 전략이 즉시 로딩이지만, 사용자의 쿼리(JPQL) 가 우선이기 때문에

JPQL 을 충실히 수행후,

글로벌 페치 전략에 의해 연관관계에 있는 엔티티나 컬렉션 엔티티가

사용 여부와 상관 없이 조회되는 상황이다.

 

예시

더보기

ManyToOne 예시

Member 를 조회(1) 하면 즉시 로딩에 의해

Member 에 연관된 Team 엔티티가 Member 조회 결과 갯수(N) 만큼 추가로 조회 된다.

-> 영속성 컨텍스트에 조회하므로, Team 을 조회하다가

동일한 Team 이 필요하면 1 차 캐시에서 조회되어 N 보다 작은 값 만큼 조회 될 수 도 있다.

(Member Table 에는 team_id 가 존재하므로 1차 캐시에서 식별 가능)

 

OneToMany 예시

Team 을 조회(1) 하면 즉시 로딩에 의해

Team 에 연관된 Member 컬렉션 엔티티가 Team 조회 결과 갯수(N) 만큼 추가로 조회 된다.

-> Team 에 속한 Member 들이 한번에 조회되는 방식

(Team Table 에는 member 에 대한 식별자가 없으므로 N 보다 작은 값 만큼 조회 될 수 없을 듯..)

 

주의 사항

Spring Data JPA 사용 시, findById() 빼고는 모두...(?)

JPQL 을 생성하여 조회하는 것이므로

N + 1 문제가 발생한다. (메서드 이름 조회, findAll(), 등등)

 

참고

Spring Data JPA 사용 시, findById() 에서는

연관된 엔티티나 컬렉션 엔티티를 함께 조회하는 쿼리로 수행된다.

이 때는 N + 1 문제가 일어나지 않는다.

(EntityManager::find 사용 됨)

 

 

글로벌 페치 전략이 지연 로딩(Lazy loading) 인 경우

어떤 엔티티를 조회 후, 연관된 엔티티나 컬렉션 엔티티를 실제 사용할 때

지연 로딩이 발생하는데 이 경우도 N + 1 문제이다.

-> 글로벌 페치 전략이 지연 로딩이므로, 어떤 엔티티를 조회 하더라도

연관 관계에 있는 엔티티나 컬렉션 엔티티는 프록시로 할당하여 반환된다.

그리고 연관 관계에 있는 엔티티나 컬렉션 엔티티를 실제 사용할 때

프록시가 초기화 된다. (지연 로딩, 쿼리 수행됨)

 

예시

더보기

ManyToOne 예시

Member 를 조회(1) 하고 Team 을 사용할 때 지연 로딩에 의해

Member 에 연관된 Team 엔티티가 Member 조회 결과 갯수(N) 만큼 추가로 조회 된다.

-> 영속성 컨텍스트에 조회하므로, Team 을 조회하다가

동일한 Team 이 필요하면 1 차 캐시에서 조회되어 N 보다 작은 값 만큼 조회 될 수 도 있다.

(Member Table 에는 team_id 가 존재하므로 1차 캐시에서 식별 가능)

 

OneToMany 예시

Team 을 조회(1) 하고 Member 를 사용할 때 지연 로딩에 의해

Team 에 연관된 Member 컬렉션 엔티티가 Team 조회 결과 갯수(N) 만큼 추가로 조회 된다.

-> Team 에 속한 Member 들이 한번에 조회되는 방식

(Team Table 에는 member 에 대한 식별자가 없으므로 N 보다 작은 값 만큼 조회 될 수 없을 듯..)

 

해결 법 1. 페치 조인(or @EntityGraph) 사용

JPQL 의 join fetch 를 사용하여

연관 관계에 있는 엔티티나 컬렉션 엔티티도 함께 조회(영속화) 한다.

N + 1 개의 쿼리가 1 개의 쿼리로 최적화 된다.

 

주의 사항 1

컬렉션 엔티티를 페치 조인 하면 페이징 쿼리를 사용할 수 없다.

페이징 쿼리로 수행되지 않고 전체 쿼리가 수행된 후

메모리에서 페이징 된다.

 

주의 사항 2

컬렉션 엔티티를 둘 이상 한번에 페치 조인 하면

MultipleBagFetchException 이 발생한다.

 

주의 사항 3

DB 의 결과와 헷갈려서 join fetch 를 사용하지 않고

일반적인 join 을 사용하면 join 대상은 영속화 되지 않는다.

 

참고 1

Hibernate 6 버전 부터는 fetch join 사용 시,

쿼리에 자동으로 distinct 가 추가되어 중복 엔티티를 걸러준다.

 

참고 2

@EntityGraph 를 사용하면 left outer join 을 사용하게 된다.

 

해결 법 2. Hibernate Batch Size 옵션 사용

어떤 글로벌 페치 전략이든 N + 1 문제가 생길 수 있는데

N + 1 개의 쿼리가 1 + 1 개의 쿼리로 최적화 될 수 있다.

 

단일 설정은 @org.hibernate.annotations.BatchSize

글로벌 설정은 spring.jpa.properties.hibernate.default_batch_fetch_size

을 이용하며, size 를 설정하여 한번에 몇개의 쿼리(N)를 1개의 쿼리로 합칠 것인지

설정할 수 있다.

 

해결 법 3. Hibernate SubSelect 기능 사용

서브 쿼리를 사용하여 N + 1 개의 쿼리를 1 + 1 개의 쿼리로 최적화 한다.

 

적용하고 싶은 연관 관계 컬렉션 엔티티에 아래 어노테이션을 적용한다.

@org.hibernate.annotations.Fetch(FetchMode.SUBSELCT)

 

주의 사항

ManyToOne 관계에서는 사용하지 못한다.

 

 

정리

1. 즉시 로딩은 사용하지 않는 것이 좋다.

-> 연관 관계에 있는 엔티티나 컬렉션 엔티티를 항상 사용하는 것이 아니기 때문

 

2. 모두 지연 로딩으로 설정하고 필요하면 페치 조인을 사용하자

-> 영속성 컨텍스트 범위를 벗어나면 지연 로딩을 수행하지 못하므로 

페치 조인을 이용한 성능 최적화를 챙기면서 미리 로딩을 하는 게 좋다.

 

3. 페치 조인을 사용하지 못하는 케이스일 경우엔 Batch 옵션을 활용하자

 

'Spring > DB, Cache 연동' 카테고리의 다른 글

@Transactional 과 Test code 고찰 - 개발자들의 생각 모음  (0) 2024.04.22
DBCP (HikariCP)  (0) 2024.04.22
Spring 과 JPA  (0) 2024.02.14
JPA 등록, 기본 키 생성 전략  (0) 2023.06.19
JPA 변경 감지와 플러시  (0) 2023.06.15