본문 바로가기
카테고리 없음

JPA에서 발생하는 N+1 의 발생 이유와 개선방법

by 트라네스 2025. 3. 18.
728x90
반응형

 이전에 공부했던 내용에 이어서 항상 나오는 N+1의 이야기를 좀 작성해 보고자 한다. JPA 가 알아서 쿼리도 작성해 주고 다 편하다고 하는데 N+1 때문에 항상 문제가 된다며 이런저런 질문지에 오르내리곤 하는 것 같다. 이번에 N+1에 대해서 확실하게 정리해 두도록 하자.

 

JPA에서 N+1 문제 발생 이유와 해결 방법

1. N+1 문제란?

JPA에서 연관관계를 가진 데이터를 조회할 때, 의도치 않게 추가적인 쿼리가 다수 발생하는 문제를 N+1 문제라고 합니다.

  • N+1이란?
    • 1: 처음 실행하는 쿼리 (예: 회원 목록 조회)
    • N: 연관된 엔티티를 조회하는 추가 쿼리 (예: 각 회원의 주문 목록 조회)

2. N+1 문제 발생 예시

예제 엔티티 (회원 & 주문 관계)

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();
}

@Entity
public class Order {
    @Id @GeneratedValue
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;
}

 

N+1 문제 발생 코드

public List<Member> findAllMembers() {
    return em.createQuery("SELECT m FROM Member m", Member.class).getResultList();
}

 

위 코드를 실행할 경우 발생하는 쿼리의 리스트 

 

첫번째로 1

SELECT * FROM member;  -- (1) 모든 회원 조회 (1개 쿼리 발생)

이제 다음에 이어지는 N

SELECT * FROM orders WHERE member_id = 1;  -- (N) 각 회원별 주문 조회
SELECT * FROM orders WHERE member_id = 2;  
SELECT * FROM orders WHERE member_id = 3;

→ 회원 수만큼 추가 조회 쿼리가 실행됨 → N+1 문제 발생!

 

3. N+1 문제 해결 방법

1) Fetch Join 사용 (가장 권장됨)

JPA의 fetch join을 사용하면 연관된 엔티티를 한 번의 SQL 조회로 함께 가져올 수 있음.

public List<Member> findAllMembersWithOrders() {
    return em.createQuery(
        "SELECT m FROM Member m JOIN FETCH m.orders", Member.class)
        .getResultList();
}

 

코드 실행 시 실행되는 쿼리

SELECT m.*, o.* FROM member m 
JOIN orders o ON m.id = o.member_id;

단 1개의 쿼리만 수행되며 데이터를 한 번에 가져올 수 있게 한다.

 

2) EntityGraph 사용

  • @EntityGraph를 사용하여 fetch join을 JPA가 자동으로 수행하게 할 수 있음.
@EntityGraph(attributePaths = {"orders"})
@Query("SELECT m FROM Member m")
List<Member> findAllWithOrders();

 

3) Batch Size 조정 (IN 절 사용)

  • @BatchSize를 활용하여 한 번의 IN 절로 여러 개의 데이터를 조회할 수 있음.
@Entity
public class Member {
    @OneToMany(mappedBy = "member")
    @BatchSize(size = 10)
    private List<Order> orders;
}

또는 application.properties에서 설정 가능

spring.jpa.properties.hibernate.default_batch_fetch_size=100

 

실행되는 SQL:

SELECT * FROM orders WHERE member_id IN (1, 2, 3, ..., 10);

 

4) QueryDSL 활용

  • QueryDSL을 사용하면 fetch join과 동적 쿼리를 쉽게 적용 가능.
public List<Member> findAllWithOrders() {
    return queryFactory.selectFrom(member)
        .leftJoin(member.orders, order).fetchJoin()
        .fetch();
}

 

 

4. N+1 문제 발생을 줄이는 개선 방법

N+1 문제 예방을 위한 체크리스트

  1. Lazy Loading vs. Eager Loading 설정 주의
    • @OneToMany(fetch = FetchType.LAZY) 기본 사용 → 필요할 때만 조회
    • @OneToMany(fetch = FetchType.EAGER)는 가급적 지양 (자동 N+1 발생 가능)
  2. Fetch Join 적극 활용
    • JOIN FETCH 사용하여 한 번의 쿼리로 연관 데이터를 가져오기
  3. Hibernate Batch Fetch Size 조정
    • spring.jpa.properties.hibernate.default_batch_fetch_size 설정 활용
    • @BatchSize(size = N) 설정하여 IN 절로 조회
  4. 성능 테스트 및 SQL 로그 확인
    • Hibernate SQL 로그를 활성화하여 불필요한 쿼리가 실행되는지 확인
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

 

    5. QueryDSL, Native Query 활용

  • 복잡한 쿼리는 QueryDSL 또는 Native Query로 직접 최적화

5. 결론

  • N+1 문제는 Lazy Loading으로 인해 추가 쿼리가 발생하는 현상
  • 해결 방법:
    • fetch join (가장 추천)
    • @EntityGraph (자동 fetch join)
    • Batch Fetch Size 조정 (IN 절 활용)
    • QueryDSL, Native Query 활용 (고급 최적화)

JPA를 사용할 때는 항상 SQL 로그를 확인하고, 필요에 따라 fetch join과 batch size 조정을 활용하여 최적화하는 것이 중요합니다! 🚀

728x90
반응형

댓글


TOP

TEL. 02.1234.5678 / 경기 성남시 분당구 판교역로