N+1 문제란?

연관 관계가 설정된 엔티티를 조회할 경우, 조회된 데이터 갯수 만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오는 현상

발생 이유?

JPA가 JPQL을 분석해서 SQL을 생성할 때 글로벌 fetch 전략을 참고하지 않고 오직 JPQL 자체만을 사용하기 때문이다.</br>

근본적인 원인은 관계형 데이터베이스와 객체지향 언어간의 패러다임 차이 때문이다.</br> 객체는 연관관계를 통해 레퍼런스를 가지고 있으면 메모리 내에서 Random Access를 통해 연관 객체에 접근할 수 있지만, RDB의 경우 Select 쿼리를 통해서만 조회할 수 있기 때문이다.

  1. fetch 전략이 즉시 로딩인 경우
    1. findAll()을 한 순간 select r from restaurant r 이라는 구문이 생성되고, 이 구문을 분석한 select * from restaurant 이라는 sql문이 생성되어 실행된다.
    2. DB결과를 받아 restaurant 엔티티의 인스턴스들을 생성한다.
    3. restaurant과 연관되어 있는 review 도 로딩한다.
    4. 영속성 컨텍스트에서 연관된 review가 있는지 확인한다.
    5. 영속성 컨텍스트에 없다면 2에서 만들어진 restaurant 인스턴스들 갯수에 맞게 select * from review where restaurant_id = ? 이라는 구문이 생성된다 (N+1 발생)
  2. fetch 전략이 지연 로딩인 경우
    1. findAll()을 한 순간 select r from restaurant r 이라는 JPQL 구문이 생성되고 해당 구문을 분석한 select * from restaurant이라는 SQL이 생성되어 실행한다.
    2. DB결과를 받아 restaurant 엔티티의 인스턴스들을 생성한다.
    3. 코드 중에서 team의 review 객체를 사용하려고 하는 시점에 영속성 컨텍스트에 연관된 review 가 있는지 확인한다.
    4. 영속성 컨텍스트에 없다면 2에서 만들어진 team 인스턴스들 갯수에 맞게 select * from review where restaurant_id = ? 이라는 sql 구문이 생성된다. (N+1 발생)

해결 방법

1. fetch join

JPQL을 사용하여 DB에서 데이터를 가져올 때, 처음부터 연관된 데이터까지 같이 가져오게 하는 방법이다. fetch join을 쓰면 sql문의 inner join구문으로 변경되어 실행되고, n+1 문제를 해결할 수 있다.

단점은 테이블의 모든 컬럼을 다 조회해야하기 때문에, 실질적으로 사용하는 칼럼수가 적다면 부담스러운 방법이 될 수 있다.

2. DTO 객체로 리턴하도록 하기

JPQL이 리턴할 DTO 클래스는 필요한 칼럼만 필드로 설정하고, 생성자를 통해 주입한다.

단점은 Repository에서 Entity가 아닌 DTO를 기준으로 조회한다는 점이고, JPQL이 복잡해지고, 다른 API에서 재사용이 힘들다는 점이다.

3. EntityGraph 어노테이션

(추가 예정..)

4. BATCH SIZE 조절

(추가 예정..)

실무에서 N+1 문제로 DB가 죽어버리는 문제를 방지하기 위해 어떻게 해야할까?