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 |