[JPA] 그 유명한 N+1 문제

N+1?

  • 연관된 엔티티를 로드할 때 발생한다.
  • 한 개의 쿼리로 N 개의 엔티티를 로드 후
    • 각 엔티티와 연관된 다른 엔티티들을 로드하기 위해
    • 추가적으로 N 번의 쿼리를 수행하게 됨.
    • 최종적으로 N + 1 의 호출이 발생하는 것이다.

예제로 알아보자

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
    private List<Order> orders = new ArrayList<Order>();
}

@Entity
@Table(name = "ORDERS")
public class Order {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    private Member member;
}
  • Member 엔티티 목록을 조회시,
    • Member 엔티티마다 연관된 Order 엔티티들을 가져오기 위한 별도의 쿼리가 수행된다.
    • 만약 10개의 Member 엔티티를 조회하면
      • 최초의 1번 쿼리 ( 10 개의 Member 를 가져오는 쿼리 )
      • Member 에 대한 10번의 추가 쿼리가 수행되어
      • 총 11번의 쿼리가 수행됨.
  • 한번 테스트코드로 확인해볼까?
    @Test
        public void nPlusOneProblemTest() {
            // Member 엔티티를 조회합니다.
            List<Member> members = memberRepository.findAll();
            
            for (Member member : members) {
                // 각 Member 엔티티에 연관된 Order 엔티티를 조회합니다.
                List<Order> orders = member.getOrders();
                for (Order order : orders) {
                    System.out.println("Order id: " + order.getId());
                }
            }
        }
    1. 단일 회원 조회의 경우
      1. Member 엔티티와 연관된 Order 컬렉션이 즉시 로딩으로 설정되어있기에, 이를 함계 조회
      1. Join 으로 한번에 들고오긴함
    1. 다중 회원 조회
      select m1_0.id,m1_0.name from member m1_0;
      • 일단 모든 멤버를 들고오긴한다.
      • 하지만..
      • 회원의 수만큼
      • 조인된 쿼리가 발사된다.
      select o1_0.member_id,o1_0.id from orders o1_0 where o1_0.member_id=1;
      select o1_0.member_id,o1_0.id from orders o1_0 where o1_0.member_id=2;

    왜 발생할까?

    • ORM 의 동작방식에 집중해야함
    • 초기 쿼리 수행 문제
      • 처음에는 Member 엔티티라면 ..연관된 엔티티를 고려치않고 주 엔티티만을 가져온다. → 이게 주요한것 같다.
    • 즉시 로딩
      • 초기 쿼리가 수행되는 즉시
      • 연관된 엔티티를 조인하여 들고올수있도록 다시 하나의 레코드에 대해 쿼리를 친다.
      • 단건의 레코드마다 → 조인한 쿼리가 발사된다.

단점은?

그냥 딱봐도 문제다 SQL 이 몇번 실행되는건지..

만약 100명의 사용자가 회원 100명에 대한 쿼리만 쳐도 101개 * 100 ⇒ 10100 회의 SQL 이 날아간다.

이건 지옥이다.

해결방안

  1. 지연 로딩의 사용
    1. FetchType.LAZY 를 통해
      1. 필요한 시점에 관련 데이터를 로드해야한다
      1. 하지만 지연로딩도 N+1 에서 아예 자유로울순없다. 후술하겠다. → 어차피 연관 엔티티에 접근을 하는 경우 계속적으로 쿼리가 발생한다.
  1. 조인 페치 사용
    1. JPQL 에서 JOIN FETCH 를 통해 필요한 데이터를 들고와야한다.
  1. 배치 사이즈 조절 ← 추천
    1. 하이버네이트 JPA 구현체에서 배치 사이즈 옵션을 통해 한 번에 쿼리에 몇개의 연관 객체를 같이 들고올 것인지 설정이 가능하다.
    1. 만약 주문 1억개고 배치 사이즈가 1천개라면
      1. 초기쿼리 + 1천개의 상품 가져오는 쿼리 + …반복 된다.

Uploaded by N2T