N + 1 문제가 일어나는 모든 경우의 수에 대해 알아보고..
한번 정리를 해보겠다.
어떤 엔티티를 조회하기 위해 쿼리(1)를 수행했는데
예상치 못한 쿼리들(N)이 추가로 수행되는 상황을 말한다.
주로 일대다, 다대일 관계에서 발생한다.
이번 포스팅에서 사용할 DB 다이어그램이다.
Team 이 여러 Member 를 가지는 관계이다.
N + 1 조회 문제가 일어나는 케이스를 살펴보겠다.
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 사용 됨)
어떤 엔티티를 조회 후, 연관된 엔티티나 컬렉션 엔티티를 실제 사용할 때
지연 로딩이 발생하는데 이 경우도 N + 1 문제이다.
-> 글로벌 페치 전략이 지연 로딩이므로, 어떤 엔티티를 조회 하더라도
연관 관계에 있는 엔티티나 컬렉션 엔티티는 프록시로 할당하여 반환된다.
그리고 연관 관계에 있는 엔티티나 컬렉션 엔티티를 실제 사용할 때
프록시가 초기화 된다. (지연 로딩, 쿼리 수행됨)
Member 를 조회(1) 하고 Team 을 사용할 때 지연 로딩에 의해
Team 을 조회(1) 하고 Member 를 사용할 때 지연 로딩에 의해
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 을 사용하게 된다.
어떤 글로벌 페치 전략이든 N + 1 문제가 생길 수 있는데
N + 1 개의 쿼리가 1 + 1 개의 쿼리로 최적화 될 수 있다.
단일 설정은 @org.hibernate.annotations.BatchSize
글로벌 설정은 spring.jpa.properties.hibernate.default_batch_fetch_size
을 이용하며, size 를 설정하여 한번에 몇개의 쿼리(N)를 1개의 쿼리로 합칠 것인지
설정할 수 있다.
서브 쿼리를 사용하여 N + 1 개의 쿼리를 1 + 1 개의 쿼리로 최적화 한다.
적용하고 싶은 연관 관계 컬렉션 엔티티에 아래 어노테이션을 적용한다.
@org.hibernate.annotations.Fetch(FetchMode.SUBSELCT)
ManyToOne 관계에서는 사용하지 못한다.
1. 즉시 로딩은 사용하지 않는 것이 좋다.
-> 연관 관계에 있는 엔티티나 컬렉션 엔티티를 항상 사용하는 것이 아니기 때문
2. 모두 지연 로딩으로 설정하고 필요하면 페치 조인을 사용하자
-> 영속성 컨텍스트 범위를 벗어나면 지연 로딩을 수행하지 못하므로
페치 조인을 이용한 성능 최적화를 챙기면서 미리 로딩을 하는 게 좋다.
3. 페치 조인을 사용하지 못하는 케이스일 경우엔 Batch 옵션을 활용하자
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 연동' 카테고리의 다른 글