N+1

Lzhtk·2025년 6월 5일

오늘은 JPA에서 발생하는 N+1 문제에 대해 알아보자❗❕


N+1 문제란?

  • N+1 문제란 JPA에서 연관 관계가 설정된 엔티티를 조회할 때 발생하는 대표적인 성능 문제이다.
  • 부모 엔티티를 조회하는 쿼리 1번 이후, 각 부모 엔티티에 연관된 자식 엔티티를 조회하기 위한 쿼리가 부모 엔티티의 개수(N)만큼 추가로 실행되는 현상을 말한다.
  • Ex ) 부모 엔티티 10개를 조회한 후에 각 부모의 자식 리스트를 접근할 때마다 쿼리가 실행되어 총 1 + 10 = 11번의 쿼리가 발생하여 N+1문제이다.

N+1 문제가 발생하는 원인

  • N+1 문제는 주로 지연 로딩( LAZY ) 환경에서 연관된 엔티티에 접근할 때 발생한다.

  • JPA는 엔티티를 조회할 때 연관된 엔티티를 즉시 조회하지 않고, 실제로 해당 엔티티에 접근하는 시점에 쿼리를 실행한다.

  • 이로 인해 부모 엔티티가 N개라면, 각 부모의 연관 엔티티에 접근할 때마다 추가적인 SELECT 쿼리가 실행되어 N+1 문제가 발생한다.

  • 하지만 N+1 문제의 본질적인 원인은 단순히 LAZY 설정 자체가 아니라 연관된 엔티티를 한 번에 조회하지 않고, 엔티티 단위로 개별 조회하는 조회 전략에 있다.

  • 즉시 로딩( EAGER ) 전략을 사용하더라도 JPQL 조회 방식에 따라 유사한 N+1 문제가 발생할 수 있다.

N+1을 해결하는 방안

  • N+1 문제를 해결하는 대표적인 방안으로 4가지를 알아보자
    1️⃣ Fetch Join 사용 ✅ ( 가장 권장 )
    • JPA의 fetch join을 사용하면 연관된 엔티티를 한 번의 쿼리로 함께 조회가 가능하다.
      @Query("SELECT p FROM Parent p JOIN FETCH p.children")
      List<Parent> findAllWithChildren();
      -> 이렇게 Fetch Join을 통해 1번의 쿼리로 부모와 자식을 모두 가져올 수 있음
    • 단, 컬렉션 Fetch Join은 페이징과 함께 사용할 수 없다는 제약이 있어 조회 목적에 따라 주의가 필요하다.
    2️⃣ Entity Graph 활용
    - 엔티티 그래프를 사용해 연관 엔티티를 미리 로딩할 수 있습니다.
    @EntityGraph(attributePaths = "children")
    List<Parent> findAll();
    • Fetch Join보다 선언적으로 사용할 수 있어 상황에 따라 유리함!
    3️⃣ Batch Size 조정
    • JPA 구현체의 batch size 옵션을 설정하면 LAZY 로딩 시 N+1이 아닌 IN 절을 사용한 여러 건의 쿼리로 최적화가 가능해진다.
      spring:
      	jpa:
          	properties:
              	hibernate.default_batch_fetch_size: 100
    4️⃣ EAGER 로딩 사용
    • EAGER로 설정하면 항상 연관 엔티티를 함께 조회하지만, 불필요한 데이터까지 로딩되거나 JPQL 조회 방식에 따라 성능 문제가 발생할 수 있어 권장되지 않는다.

실무적인 권장 사항

  • 실무에서는 연관 관계 매핑 시 기본적으로 LAZY 로딩을 사용하는 것이 좋다.
  • 그리고 실제 조회 요구 사항에 따라 Fetch Join이나 Entity Graph를 선택적으로 적용하여 성능을 최적화하는 것이 바람직하다.

마무리 🔚

JPA를 사용할 때 가장 자주 마주치는 성능 이슈 중 하나인 N+1에 대해 알아보았다.
단순히 로딩 전략만으로 판단하기보다는, 조회 패턴과 쿼리 전략을 함께 고려해 상황에 맞는 해결 방법을 선택하도록 하자💯

1개의 댓글

comment-user-thumbnail
2025년 6월 16일

깔끔한 글~~ 잘봤습니다!

답글 달기