N+1 문제란?

JPA를 사용할 때 흔히 발생하는 성능 이슈 중 하나는 N+1 문제이다.

예를 들어, 게시글 목록 10개를 조회하면서 각 게시글의 작성자 정보를 함께 사용해야 하는 상황을 생각해보자.

  1. postRepository.findAll() 호출 시, 게시글 10개는 한 번의 쿼리로 가져온다.
  2. 하지만 이후 루프에서 각 게시글에 대해 post.getUser()를 호출하면, 작성자 정보를 가져오기 위한 쿼리가 게시글 수만큼 추가로 실행된다.

즉, 게시글 10개를 조회하면 다음과 같은 쿼리 흐름이 발생한다.

왜 한 번에 가져오지 못할까?

JPA는 엔티티 연관관계에서 지연 로딩(LAZY) 을 사용하게되면 post.getUser()처럼 연관된 엔티티에 실제로 접근할 때까지는 해당 데이터를 불러오지 않는다. (현재 시점에서 필요한 데이터만 불러옴)

이로 인해 작성자가 동일한 게시글이 있더라도, 캐시에 존재하지 않는 경우에는 중복 쿼리가 발생한다.

image.png

1. 게시글 목록 15개 조회 (쿼리 1번)

select
    pe1_0.id,
    pe1_0.content,
    pe1_0.created_at,
    pe1_0.created_by,
    pe1_0.deleted_at,
    pe1_0.deleted_by,
    pe1_0.is_deleted,
    pe1_0.modified_at,
    pe1_0.modified_by,
    pe1_0.title,
    pe1_0.user_id
from
    posts pe1_0
order by
    pe1_0.created_at desc
limit ?, ?

2. 게시글 총 개수 조회 (카운팅 쿼리)

select
    count(pe1_0.id)
from
    posts pe1_0

3. 게시글의 작성자 개별 조회 (쿼리 최대 N번 발생)

Hibernate: 
    select
        ue1_0.id,
        ue1_0.created_at,
        ue1_0.created_by,
        ue1_0.deleted_at,
        ue1_0.deleted_by,
        ue1_0.email,
        ue1_0.is_deleted,
        ue1_0.modified_at,
        ue1_0.modified_by,
        ue1_0.nickname,
        ue1_0.password,
        ue1_0.role,
        ue1_0.username 
    from
        users ue1_0 
    where
        ue1_0.id=?
Hibernate: 
    select
        ue1_0.id,
        ue1_0.created_at,
        ue1_0.created_by,
        ue1_0.deleted_at,
        ue1_0.deleted_by,
        ue1_0.email,
        ue1_0.is_deleted,
        ue1_0.modified_at,
        ue1_0.modified_by,
        ue1_0.nickname,
        ue1_0.password,
        ue1_0.role,
        ue1_0.username 
    from
        users ue1_0 
    where
        ue1_0.id=?
Hibernate: 
    select
        ue1_0.id,
        ue1_0.created_at,
        ue1_0.created_by,
        ue1_0.deleted_at,
        ue1_0.deleted_by,
        ue1_0.email,
        ue1_0.is_deleted,
        ue1_0.modified_at,
        ue1_0.modified_by,
        ue1_0.nickname,
        ue1_0.password,
        ue1_0.role,
        ue1_0.username 
    from
        users ue1_0 
    where
        ue1_0.id=?
        
...