흰 스타렉스에서 내가 내리지

[JPA] N+1 문제 (실은 1+N 문제) 본문

JPA

[JPA] N+1 문제 (실은 1+N 문제)

주씨. 2024. 4. 25. 19:03
728x90

즉시 로딩JPQL 을 실행할 때 N+1 문제가 발생할 수 있다.

그럼, 지연 로딩일 때는 N+1 문제가 발생하지 않는가?

 

 

@Entity
public class Order{
    ...
    
    @ManyToOne(fetch = FetchType.EAGER)
    private Member member;
    
    ...
}

 

  • 주문 엔티티를 조회하면 연관된 member 엔티티도 항상 함께 로딩된다. 

 

# 글로벌 페치 전략에 즉시 로딩 사용 시 단점

  • 사용하지 않는 엔티티를 로딩한다.
  • N+1 문제가 발생한다.

 

 

# N+1 문제

  • em.find() 메소드로 엔티티를 조회할 때 연관된 엔티티를 로딩하는 전략이 즉시 로딩이면, 데이터베이스에 JOIN 쿼리를 사용해서 한 번에 연관된 엔티티까지 조회한다. 
Order order = em.find(Order.class, 1L);

// SQL
select o.*, m.*
from Order o 
left outer join Member m on o.MEMBER_ID = m.MEMBER_ID
where o.id=1

 

  • 실행된 SQL 을 보면 즉시 로딩으로 설정한 member 엔티티를 JOIN 쿼리로 함께 조회한다.
  • 문제는 JPQL 을 사용할 때 발생한다.
List<Order> orders = 
    em.createQuery("select o from Order o", Order.class)
    .getResultList();
    
// SQL
select * from Order 		-- JPQL 로 실행된 SQL
select * from Member where id=? -- EAGER 로 실행된 SQL
select * from Member where id=? -- EAGER 로 실행된 SQL
select * from Member where id=? -- EAGER 로 실행된 SQL
select * from Member where id=? -- EAGER 로 실행된 SQL

 

  • JPA 가 JPQL 을 분석해서 SQL 을 생성할 때는 글로벌 페치 전략을 참고하지 않고 오직 JPQL 자체만 사용한다.
    • 따라서 즉시로딩이든 지연로딩이든 구분하지 않고 JPQL 쿼리 자체에 충실하게 SQL 을 만든다.

 

다음과 같은 순서로 동작한다.

  1. select o from Order o JPQL 을 분석해서 select * from Order SQL을 생성한다.
  2. 데이터베이스에서 결과를 받아 order 엔티티 인스턴스들을 생성한다.
  3. order.member 의 글로벌 페치 전략이 즉시 로딩이므로 order 를 로딩하는 즉시 연관된 member 도 로딩해야 한다.
  4. 연관된 member 를 영속성 컨텍스트에서 찾는다
  5. 만약 영속성 컨텍스트에 없으면 SELECT * FROM MEMBER WHERE id=? SQL 을 조회한 order 엔티티 수만큼 실행한다.
  • 만약 조회한 order 엔티티가 10개이면 member 를 조회하는 SQL 도 10번 실행한다.
  • 이처럼 처음 조회한 데이터 수만큼 다시 SQL 을 사용해서 조회하는 것을 N+1 문제라 한다. 
  • N+1 이 발생하면 SQL 이 상당히 많이 호출되므로 조회 성능에 치명적인다. 
  • 이런 N+1 문제는 JPQL 페치 조인으로 해결할 수 있다.

 

 

결론 :

N+1 문제는, 연관된 엔티티가 즉시로딩인 엔티티를 JPQL 로 호출할 때,

조회한 데이터 수(N)만큼 다시 SQL 을 사용해서 조회하는 것을 말한다.

이는 JPQL 페치 조인으로 해결할 수 있다.

 

 

그럼, 지연 로딩일 때는 N+1 문제가 발생하지 않는가?

for (Member member : members){
    System.out.println("member = " + member.getOrders().size());
}

 

문제는 위처럼 모든 회원에 대해 연관된 주문 컬렉션을 사용할 떄 발생한다.

주문 컬렉션을 초기화 하는 수만큼 다음 SQL 이 실행될 수 있다.

회원이 5명이면 회원에 따른 주문도 5번 조회된다.

SELECT * FROM ORDERS WHERE MEMBER_ID = 1
SELECT * FROM ORDERS WHERE MEMBER_ID = 2
SELECT * FROM ORDERS WHERE MEMBER_ID = 3
SELECT * FROM ORDERS WHERE MEMBER_ID = 4
SELECT * FROM ORDERS WHERE MEMBER_ID = 5

 

이것도 결국 N+1 문제다. 

N+1 문제는 즉시 로딩과 지연 로딩일 때 모두 발생할 수 있다.

 

https://thisisjoos.tistory.com/715

 

[JPA] N+1 문제를 피할 수 있는 다양한 방법

1. 페치 조인 사용가장 일반적인 방법select m from Member m join fetch m.ordersSELECT M.*, O.* FROM MEMBER MINNER JOIN ORDERS O ON M.ID=O.MEMBER_ID 일대다 조인을 했으므로 중복된 결과가 나타날 수 있다.따라서 JPQL 의 DI

thisisjoos.tistory.com