-
[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 객체가 아닌 실제 객체를 전달받았음을 알 수 있습니다.
- JPQL이 SQL로 변경되면서 Order 테이블 조회합니다. (쿼리문 1번 발생)