N+1 문제는 연관관계가 설정된 1개의 객체를 조회할 때, 추가로 N개의 쿼리가 발생하는 현상을 말한다.
직접 쿼리문을 만들어 조회할 때는 하나의 쿼리만 실행된다.
하지만 JPA를 통해 객체를 조회하면, 연관된 다른 객체를 함께 조회하려는 과정에서 N+1 문제가 발생할 수 있다.
// 회원과 주문 (Member - Order: 1:N)
// Member.java
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
// Order.java
@ManyToOne
private Member member;
Member와 Order로 된 1:N 관계를 예시로 설명해보자!
글로벌 패치 전략을 즉시로딩으로 설정한 후,
findAll() 메소드를 사용하여 Member 객체를 조회하면, 다음과 같은 쿼리가 실행된다.
-select m from Member m -> 1번 실행
Member의 Order를 조회하는 쿼리 -> 회원 수(N)만큼 실행즉, JPA는
Member를 조회하는 과정에서 해당 멤버가 가진Order까지 즉시 로딩하려고 하며, 이로 인해 총 N + 1개의 쿼리가 발생한다.
지연로딩으로 설정하면 프록시 객체가 주입되니, N+1 문제를 해결할 수 있을까?
findAll() 메소드를 사용하여 Member 객체를 조회하면 1개의 쿼리만 생성된다.
하지만 이후 member.getOrders()와 같이 Member의 Order를 조회하는 순간, 각 프록시에 대한 쿼리(N)가 발생하게 된다.
결국 총 N + 1 쿼리 발생가 발생하며, 지연 로딩도 N+1 문제를 피할 수 없다.
Fetch Join과 함께 지연로딩을 사용하면 N+1문제를 해결할 수 있다.
@Query("select m from Member m join fetch m.orders")
List<Member> findAllJPQLFetch();
fetch join을 사용하면 지연로딩으로 설정 되어있는 연관관계를 Join 쿼리로 한번에 조회할 수 있다.
@EntityGraph를 사용하면 JPQL을 직접 쓰지 않고, fetch join과 유사하게 동작하도록 할 수 있다.
@EntityGraph(attributePaths = {"orders"})
List<Member> findAll();
지연로딩을 부분적으로 즉시로딩으로 전환하는 기능이다.
여러 다대일 연관관계를 한번에 Join해올 수 있다.