JPA로 양방향 연관관계를 설정하면 N+1 문제가 발생할 수 있다는 얘기를 많이 들어봤다..
이번 프로젝트에서도 양방향 연관관계를 설정한 만큼
이번 기회에 N+1 문제가 무엇이고 해결 방법에는 무엇이 있는지 정리하고자 한다!
N+1 문제
`N+1 문제`는 JPA에서 발생할 수 있는 데이터베이스 쿼리 최적화와 관련된 중요한 성능 문제 중 하나이다.
내가 진행했던 프로젝트에서 예를 들면 Shop - Menu 엔티티들은 서로 연관관계를 맺는다
Shop은 여러 개의 Menu를 가지니까 Shop이 `@OneToMany`로 Menu 필드를 가지고 Menu가 `@ManyToOne`로 Shop 필드를 가질 수 있을 것이다. (쉽게 말하면 Shop-Menu -> 일대다 관계)
그럼 특정 Shop의 Menu 필드를 조회하는 시나리오는 아래와 같다.
1. 특정 Shop 엔티티를 조회하는 쿼리가 실행된다.
SELECT * FROM Shop
2. 조회된 각 Shop 엔티티에 연관된 Menu 엔티티들을 가져오기 위해 N개의 추가 쿼리가 실행된다.
SELECT * FROM Menu WHERE shop_id = ?
Shop 수가 N개라면, 각 Shop에 대해 관련된 Menu를 가져오기 위해 위와 같은 쿼리가 N번 나간다.
결과적으로 N개의 Shop에 대해 N번의 추가 쿼리가 실행되므로 총 N+1번의 쿼리가 실행된다.
이 문제는 `FetchType.Eager`와 `FetchType.Lazy` 둘 모두에서 발생할 수 있다.
쿼리가 실행되는 시점만 다를 뿐, 연관된 엔티티가 사용된다면 `FetchType.Lazy`가 사용되어도 N+1 문제를 피할 수 없다.
JPA를 사용하여 데이터를 조회할 때, `JPQL`은 SQL로 변환되어 데이터베이스에서 실행된다.
이때, `JPQL`은 기본적으로 연관된 엔티티를 어떻게 로드할지에 대한 로딩 전략(Eager 또는 Lazy)을 따른다.
따라서 `JPQL`을 통해 엔티티를 조회한 후, 연관된 엔티티의 로딩 전략에 따라 N개의 추가 쿼리가 발생할 수 있다.
결국 N+1 문제는 JPQL이 자동으로 조인을 수행하지 않기 때문에 발생한다. 그렇다면 JPQL은 왜 알아서 조인하지 않는 것일까?
JPQL이 자동으로 조인을 수행하지 않는 이유는 주로 성능 최적화와 개발자의 명시적 제어를 가능하게 하기 위함이다.
JPA는 다양한 상황에 맞게 `유연한 데이터 로딩 전략`을 지원하며, 개발자가 언제 조인을 사용할지 `명시적`으로 결정하게 함으로써 불필요한 데이터 로딩이나 성능 저하를 방지한다.
N+1 문제 해결 방법
1. fetch join 사용
`JOIN FETCH`를 사용하여 연관된 엔티티를 함께 조회함으로써 추가 쿼리 발생을 방지할 수 있다.
SELECT a FROM Menu m JOIN FETCH m.shop
`@Query`로 직접 쿼리를 작성한다.
2. @BatchSize 사용
Hibernate의 `@BatchSize` 애노테이션을 사용하여 한 번에 여러 개의 연관 엔티티를 가져오도록 설정할 수 있다.
@BatchSize(size = 10)
private List<Menu> menus;
이렇게 하면 N번의 쿼리가 N/10번으로 줄어들어 성능이 개선될 수 있다.
해당 옵션은 정확히 N+1 문제를 해결하는 방법은 아니고 N+1 문제가 발생하더라도 1+1 와 같이 한 번만 더 조회하는 방식으로 작동한다.
select * from menu where shop_id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
이렇게 size만큼 in 절로 쿼리가 나간다.
위와 같이 컬럼에 옵션을 줄수도 있고 `application.yml` 설정 파일에 적용할 수도 있다.
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 1000
대부분의 DB IN 절의 최대 개수 값이 1000이라 1000으로 설정한다고 한다.
3. EntityGraph 사용
JPA 2.1부터 제공되는 `EntityGraph`를 사용하여 특정 쿼리에서 `FetchType`을 `EAGER`로 오버라이드할 수 있다.
하지만, 복잡한 경우 사용하기 어려울 수 있으므로 권장하지는 않는다.
'공부 > Spring' 카테고리의 다른 글
[Spring Cloud] Gateway PreFilter에 로그인 추가하기 (OAuth2 + JWT) (0) | 2024.08.05 |
---|---|
[Spring Cloud] API Gateway (Spring Cloud Gateway) (0) | 2024.08.05 |
[Spring Cloud] 서킷 브레이커 (Resilience4j) (0) | 2024.08.05 |
[Spring Cloud] 클라이언트 사이드 로드 밸런싱 (FeignClient와 Ribbon) (0) | 2024.08.05 |
[Spring Cloud] MSA Spring Cloud (Eureka) (0) | 2024.08.01 |
JPA로 양방향 연관관계를 설정하면 N+1 문제가 발생할 수 있다는 얘기를 많이 들어봤다..
이번 프로젝트에서도 양방향 연관관계를 설정한 만큼
이번 기회에 N+1 문제가 무엇이고 해결 방법에는 무엇이 있는지 정리하고자 한다!
N+1 문제
N+1 문제
는 JPA에서 발생할 수 있는 데이터베이스 쿼리 최적화와 관련된 중요한 성능 문제 중 하나이다.
내가 진행했던 프로젝트에서 예를 들면 Shop - Menu 엔티티들은 서로 연관관계를 맺는다
Shop은 여러 개의 Menu를 가지니까 Shop이 @OneToMany
로 Menu 필드를 가지고 Menu가 @ManyToOne
로 Shop 필드를 가질 수 있을 것이다. (쉽게 말하면 Shop-Menu -> 일대다 관계)
그럼 특정 Shop의 Menu 필드를 조회하는 시나리오는 아래와 같다.
1. 특정 Shop 엔티티를 조회하는 쿼리가 실행된다.
SELECT * FROM Shop
2. 조회된 각 Shop 엔티티에 연관된 Menu 엔티티들을 가져오기 위해 N개의 추가 쿼리가 실행된다.
SELECT * FROM Menu WHERE shop_id = ?
Shop 수가 N개라면, 각 Shop에 대해 관련된 Menu를 가져오기 위해 위와 같은 쿼리가 N번 나간다.
결과적으로 N개의 Shop에 대해 N번의 추가 쿼리가 실행되므로 총 N+1번의 쿼리가 실행된다.
이 문제는 FetchType.Eager
와 FetchType.Lazy
둘 모두에서 발생할 수 있다.
쿼리가 실행되는 시점만 다를 뿐, 연관된 엔티티가 사용된다면 FetchType.Lazy
가 사용되어도 N+1 문제를 피할 수 없다.
JPA를 사용하여 데이터를 조회할 때, JPQL
은 SQL로 변환되어 데이터베이스에서 실행된다.
이때, JPQL
은 기본적으로 연관된 엔티티를 어떻게 로드할지에 대한 로딩 전략(Eager 또는 Lazy)을 따른다.
따라서 JPQL
을 통해 엔티티를 조회한 후, 연관된 엔티티의 로딩 전략에 따라 N개의 추가 쿼리가 발생할 수 있다.
결국 N+1 문제는 JPQL이 자동으로 조인을 수행하지 않기 때문에 발생한다. 그렇다면 JPQL은 왜 알아서 조인하지 않는 것일까?
JPQL이 자동으로 조인을 수행하지 않는 이유는 주로 성능 최적화와 개발자의 명시적 제어를 가능하게 하기 위함이다.
JPA는 다양한 상황에 맞게 유연한 데이터 로딩 전략
을 지원하며, 개발자가 언제 조인을 사용할지 명시적
으로 결정하게 함으로써 불필요한 데이터 로딩이나 성능 저하를 방지한다.
N+1 문제 해결 방법
1. fetch join 사용
JOIN FETCH
를 사용하여 연관된 엔티티를 함께 조회함으로써 추가 쿼리 발생을 방지할 수 있다.
SELECT a FROM Menu m JOIN FETCH m.shop
@Query
로 직접 쿼리를 작성한다.
2. @BatchSize 사용
Hibernate의 @BatchSize
애노테이션을 사용하여 한 번에 여러 개의 연관 엔티티를 가져오도록 설정할 수 있다.
@BatchSize(size = 10)
private List<Menu> menus;
이렇게 하면 N번의 쿼리가 N/10번으로 줄어들어 성능이 개선될 수 있다.
해당 옵션은 정확히 N+1 문제를 해결하는 방법은 아니고 N+1 문제가 발생하더라도 1+1 와 같이 한 번만 더 조회하는 방식으로 작동한다.
select * from menu where shop_id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
이렇게 size만큼 in 절로 쿼리가 나간다.
위와 같이 컬럼에 옵션을 줄수도 있고 application.yml
설정 파일에 적용할 수도 있다.
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 1000
대부분의 DB IN 절의 최대 개수 값이 1000이라 1000으로 설정한다고 한다.
3. EntityGraph 사용
JPA 2.1부터 제공되는 EntityGraph
를 사용하여 특정 쿼리에서 FetchType
을 EAGER
로 오버라이드할 수 있다.
하지만, 복잡한 경우 사용하기 어려울 수 있으므로 권장하지는 않는다.
'공부 > Spring' 카테고리의 다른 글
[Spring Cloud] Gateway PreFilter에 로그인 추가하기 (OAuth2 + JWT) (0) | 2024.08.05 |
---|---|
[Spring Cloud] API Gateway (Spring Cloud Gateway) (0) | 2024.08.05 |
[Spring Cloud] 서킷 브레이커 (Resilience4j) (0) | 2024.08.05 |
[Spring Cloud] 클라이언트 사이드 로드 밸런싱 (FeignClient와 Ribbon) (0) | 2024.08.05 |
[Spring Cloud] MSA Spring Cloud (Eureka) (0) | 2024.08.01 |