티스토리 뷰
들어가기 앞서, 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 객체가 아닌 실제 객체를 전달받았음을 알 수 있습니다.