ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JPA] fetch = EAGER 사용시, JPQL에서 N+1 문제 발생
    legacy/JPA 2024. 1. 13. 01:39

     

    들어가기 앞서, Proxy와 지연로딩의 개념을 알아야 합니다.

    여기를 참고해 주세요!

    N + 1 문제란?

    JPQL을 사용하여 Entity를 조회할 때, 연관관계에 있는 Entity를 같이 조회할 때 발생하는 문제입니다. 연관관계에 있는 Entity를 즉시로딩(fetch = EAGER)로 조회하면 추가적인 쿼리문이 발생합니다. 즉, 조회된 Entity에 연관된 N개의 Entity를 조회하는 쿼리문이 N번 발생합니다.

     

    예를 들어, Member Entity와 Order Entity가 존재합니다. Member Entity와 Order Entity는 1:N 관계라고 가정합시다.

    Member Entity

    @Entity
    @Getter @Setter
    public class Member {
    
        @Id @GeneratedValue
        @Column(name = "member_id")
        private Long id;
    
        private String name;
    
        @Embedded
        private Address address;
    
        @OneToMany(mappedBy = "member")
        private List<Order> orders = new ArrayList<>();
    }

     

     

    Order Entity

    @Entity
    @Table(name = "orders")
    @Getter @Setter
    public class Order {
    
        @Id @GeneratedValue
        @Column(name = "order_id")
        private Long id;
    
        @ManyToOne(fetch = EAGER)
        @JoinColumn(name = "member_id")
        private Member member;
    }

     

    DB

     

    JPQL을 사용하여 Order Entity를 조회해 보겠습니다.

    @Test
    void eagerTest() {
        List<Order> orders = em.createQuery("select o from Order o", Order.class)
                .getResultList();
    }

    아래와 같이 3번의 쿼리문이 발생합니다. 

    즉시로딩 사용

     

    예상했던 쿼리문은 1번이었는데 왜 3번의 쿼리문이 발생한 것일까요? JPQL을 사용하여 Entity를 조회하면 fetch = EAGER인 연관관계 Entity를 조회하는 추가 쿼리문이 발생하기 때문입니다. 

    // JPQL
    List<Order> orders = em.createQuery("select o from Order o", Order.class)
            .getResultList();
            
    // SQL        
    select * from Order
    • JPQL이 SQL로 변경되면서 Order 테이블 조회합니다. (쿼리문 1번 발생)
      • id가 4와 11인 2건이 조회됩니다.
    • Order 테이블과 매핑된 Order Entity를 확인합니다.
      • Order Entity에 Member Entity와 연관관계에 있음을 알게 됩니다.
    • 각 Order Entity마다 Member Entity를 조회하는 추가 쿼리문을 발생시킵니다.
      • id가 4인 Order Entity와 연관관계에 있는 id가 1인 Member Entity를 조회합니다. (쿼리문 1번 발생)
      • id가 11인 Order Entity와 연관관계에 있는 id가 8인 Member Entity를 조회합니다. (쿼리문 1번 발생)

    따라서 총 3번의 쿼리문이 발생하게 됩니다. 즉시로딩을 사용하였기 때문에 Entity 조회 시에 연관관계에 있는 Entity를 같이 조회해야 합니다.

    해결 방법

    모든 연관관계에 지연 로딩(fetch = LAZY)을 사용하고, fetch join을 사용합니다.

    지연로딩을 사용하여 필요한 Entity만 DB에서 조회하고, 연관관계에 있는 Entity도 같이 조회가 필요하다면 fetch join을 사용하여 쿼리문을 1번만 호출하여 조회합니다.

     

    Order Entity
    fetch = EAGER -> fetch = LAZY

    @Entity
    @Table(name = "orders")
    @Getter @Setter
    public class Order {
    
        @Id @GeneratedValue
        @Column(name = "order_id")
        private Long id;
    
        // @ManyToOne(fetch = EAGER)    
        @ManyToOne(fetch = LAZY)
        @JoinColumn(name = "member_id")
        private Member member;
    }

     

    일단 JPQL을 사용하여 지연 로딩으로 변경한 Order Entity를 조회해 봅시다. Order Entity와 연관관계에 있는 Member Entity의 클래스를 조회하니 Proxy 객체가 저장되어 있어 있습니다.

    @Test
    void eagerTest() {
        List<Order> orders = em.createQuery("select o from Order o", Order.class)
                .getResultList();
        System.out.println("orders.get(0).getMember().getClass() = " + orders.get(0).getMember().getClass());
    }

    프록시 객체

     

    이번엔 fetch join을 사용한 JPQL을 사용해봅시다.

    Order Enttiy와 연관관계에 있는 Member Entity를 쿼리문 1번으로 함께 조회하게 됩니다.

    또한, Proxy 객체가 아닌 실제 객체를 전달받았음을 알 수 있습니다.

     

Designed by Tistory.