• JPA N+1 문제

    2025. 1. 19.

    by. hyunji1109

     

    N+1 문제
    JPA를 사용할 때 성능 저하를 일으키는 대표적인 문제 중 하나
    주로 연관된 엔티티를 조회할 때 발생
    의도치 않게 너무 많은 쿼리가 실행되는 상황

     

    • 첫 번째 1: 처음 부모 엔티티를 조회하는 쿼리
    • 나머지 N: 부모 엔티티와 연관된 자식 엔티티를 개별적으로 조회하는 쿼리

     


    IF) Member 엔티티와 Order 엔티티가 @OneToMany로 연관 관계를 가진다고 가정

    @Entity
    public class Member {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
    
        @OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
        private List<Order> orders = new ArrayList<>();
    }
    
    @Entity
    public class Order {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String product;
    
        @ManyToOne(fetch = FetchType.LAZY)
        private Member member;
    }

     

    List<Member> members = em.createQuery("SELECT m FROM Member m", Member.class)
                             .getResultList();
    for (Member member : members) {
        System.out.println(member.getOrders().size()); // 각 회원의 주문 수를 출력
    }

     

    쿼리 동작 과정

    • 부모 엔티티(Member) 조회
      • 1개 쿼리
    SELECT * FROM Member;

     

    출력 예)

    Hibernate: 
        SELECT
            member0_.id AS id1_0_,
            member0_.name AS name2_0_
        FROM
            Member member0_
    • 각 부모 엔티티와 연관된 자식 엔티티(Order)를 조회
      • N개 쿼리

    두 번째 쿼리: 회원(Member) 조회

    SELECT * FROM Order WHERE member_id = 2;

     

    출력 예)

    Hibernate: 
        SELECT
            orders0_.id AS id1_1_,
            orders0_.product AS product2_1_,
            orders0_.member_id AS member_i3_1_
        FROM
            Order orders0_
        WHERE
            orders0_.member_id = 2

     

     

    JPA에서 N+1 문제가 발생하면 회원 수(부모 엔티티 수)만큼 쿼리가 추가로 실행

     

    쿼리 실행

    • SELECT * FROM Member (회원 조회)
    • 각 Member마다 SELECT * FROM Order WHERE member_id = ? 실행 (회원마다 주문 조회)

     

    문제의 원인

    • 지연 로딩(Lazy Loading)
      • @OneToMany, @ManyToOne과 같은 연관 관계에서 기본적으로 지연 로딩을 사용하면, 연관된 엔티티를 사용할 때 추가 쿼리가 실행
    • JPQL의 동작 방식
      • JPQL로 부모 엔티티만 조회하면, 자식 엔티티는 로딩되지 않고 프록시로 대체
      • 실제 접근 시 쿼리가 실행

     

    해결 방법

    1. 페치 조인(Fetch Join) 사용

    • 부모와 자식을 한 번에 조회
    List<Member> members = em.createQuery(
        "SELECT m FROM Member m JOIN FETCH m.orders", Member.class)
        .getResultList();

     

    출력 예)

    SELECT m.*, o.* 
    FROM Member m
    JOIN Order o ON m.id = o.member_id;
    • 한 번의 쿼리로 Member와 Order를 모두 조회

     

    2. @EntityGraph 사용

    • JPA에서 엔티티 그래프를 정의해 해결
    @Entity
    @NamedEntityGraph(
        name = "Member.withOrders",
        attributeNodes = @NamedAttributeNode("orders")
    )
    public class Member {
        ...
    }

     

    List<Member> members = em.createNamedQuery("SELECT m FROM Member m", Member.class)
                             .setHint("javax.persistence.fetchgraph", em.getEntityGraph("Member.withOrders"))
                             .getResultList();

     

    3. 배치 크기 설정

    • JPA의 글로벌 설정 또는 연관 관계에 배치 크기를 지정
    • @BatchSize 또는 hibernate.default_batch_fetch_size 설정
    @OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
    @BatchSize(size = 100)
    private List<Order> orders;

     

    • 실행 쿼리
      • 처음 Member 조회 쿼리 1번
      • Order 조회 쿼리 1번으로 100명에 대한 데이터를 가져옴

     

    N+1 문제를 그대로 둔다면

    • 성능 저하
      • 쿼리 실행 횟수가 많아질수록 데이터베이스 부하
    • 유지 보수 어려움
      • 코드 변경 없이 연관된 데이터가 늘어나면 성능 문제
    • 실무 적합성
      • 실제 프로젝트에서는 대규모 데이터 조회가 빈번히 발생

    'CS > 웹개발' 카테고리의 다른 글

    TDD  (0) 2025.01.23
    Docker  (0) 2025.01.20
    CI/CD  (1) 2025.01.16
    JPA Dirty Checking  (0) 2025.01.13
    google.com을 검색했을 때 일어나는 과정  (0) 2025.01.08

    댓글