N + 1 Problem

남예준·2025년 10월 9일

배경

프로젝트를 진행 중 기존의 주문 상태(Order Status)는 Order 테이블이 아닌 OrderHistory 테이블에서 관리하고 있음.

이는 주문 프로세스 흐름에 따라 실시간으로 바뀌는 주문 상태값을 OrderHistory에서 점이력 형식으로(주문 프로세스 추적 및 최신값을 가져오는 목적) 관리하기 위함인데 N+1 이슈가 발생한다는 문제점이 있음.

정의

특정 요청이 1 개의 쿼리로 처리되길 기대했는데 N 개의 추가 쿼리가 발생하는 현상

선수 지식으로 영속성 컨텍스트, 프록시, ORM에 대해서 알고 있으면 좋다고 한다.

예시

팀 : 멤버 = 1 : N의 관계(OneToMany)에서 팀 리스트를 조회하고 그 팀에 할당된 멤버들을 조회하려면 어쨋거나 팀 개수만큼 멤버를 조회해야 하므로 1(처음 팀 리스트 조회) + N(팀의 개수만큼 멤버 조회)가 된다.

  • Lazy 로딩으로 설정
    1. 먼저 팀 리스트를 조회한다. ⇒ N개의 팀을 1번의 쿼리로 가져옴

    2. 팀 리스트 당 각 멤버에 대해 Proxy 객체로 연관 객체를 가지고 있는다.

    3. 멤버를 조회할 시 1차 캐시(영속성 컨텍스트)에서 존재하는지 확인

    4. 없으므로 (select * from 멤버 where 팀 = ~ * N개)

      Fetch Join, EntityGraph 으로 해결 가능

    • 연관된 엔터티나 컬렉션을 한 번에 같이 조회하는 기능으로 연관된 엔터티까지 한 번에 영속성 컨텍스트에 올려버린다.
    • select 팀.*, 멤버.* from 팀 left join fetch 멤버
    • 진짜 객체를 같이 가지고 옴. ⇒ 따라서 추가 N 번의 조회가 사라짐.
  • Eager 로딩으로 설정
    1. 먼저 JPQL은 쿼리를 만들 때 팀에 연관 관계가 있는 엔터티는 신경을 쓰지 않고 조회 대상이 되는 Entity를 기준으로만 쿼리를 생성한다.
    2. 그렇게 팀에 대한 리스트를 DB에서 가져 온 이후에 글로벌 패치 전략을 확인한다.
    3. 이제서야 Eager라는 것을 알고 N 번의 추가 쿼리가 발생한다.
    • 애초에 즉시 로딩은 좀 지양되고 있기 때문에 잘 안 쓴다.

멤버 : 팀= N : 1의 관계(ManyToOne)에서 팀 리스트를 조회하고 그 팀에 할당된 멤버들을 조회하려면 어쨋거나 팀 개수만큼 멤버를 조회해야 하므로 1(처음 팀 리스트 조회) + N(팀의 개수만큼 멤버 조회)가 된다.

  • 팀을 조회의 기준으로 뒀을 때는 똑같을 수밖에 없다. 왜냐하면 팀에서는 멤버에 대한 연관관계를 가지고 있지 않기 때문이다. Eager나 Lazy나 동일하다.

ManyToOne의 상황에서도 비슷한 경우가 나올 수 있다.

Fetch Join, EntityGraph로 해결을 많이 한다고 한다.

0개의 댓글