공부/Spring

Springboot JPA N+1 문제와 해결법 (JPQL, FetchType)

린구 2024. 9. 2. 00:40
반응형

 

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`로 오버라이드할 수 있다.

하지만, 복잡한 경우 사용하기 어려울 수 있으므로 권장하지는 않는다.

 

 


 

 

 

반응형