- Spring Data JPA (Hibernate), N + 1 Query Problem2024년 02월 20일
- starryeye
- 작성자
- 2024.02.20.오후10:18
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 연동' 카테고리의 다른 글
DBCP 설정 값 정하기 (0) 2024.05.01 DBCP (HikariCP) (0) 2024.04.22 Spring 과 JPA (0) 2024.02.14 JPA 등록, 기본 키 생성 전략 (0) 2023.06.19 JPA 변경 감지와 플러시 (0) 2023.06.15 다음글이전글이전 글이 없습니다.댓글