본문 바로가기

JAVA

JPA N+1 문제: 실무에서 어떻게 다룰까?

JPA N+1 이란?

요청이 1개의 쿼리로 처리되길 기대했는데, N개의 추가 쿼리가 발생하는 현상

 

지연로딩(Lazy)

https://ict-nroo.tistory.com/132

로딩되는 시점에 Lazy 로딩 설정이 되어있는 연관된 엔티티(그림 기준 team)는 프록시 객체로 가져오는 방법이다.

물론, 즉시로딩을 지연로딩으로 변경해도 N+1에서 자유로울 수는 없다.

즉시로딩이 아니기에 조회와 동시에 해당 건 수만큼 연관된 정보를 조회해오지는 않겠지만, 해당 테이블에서 연관 정보를 사용하는 시점에서는 동일하게 불러오게 된다.

참고로 @OneToMany와 @ManyToMany는 기본이 지연 로딩(LAZY)이다.

 

보통 이런 경우에서는

Lazy로딩을 사용한다면, SELECT쿼리가 2번 나간다. (DB를 두 번 찌름)

이 때는 용도가 명확하기에 즉시 로딩 (EAGER) 전략을 사용해서 함께 조회하면 된다.

 

즉시로딩(Eager)

@ManyToOne, @OneToOne과 같이 @XXXToOne 어노테이션들에서 가져가는기본 전략으로 가져가는 방식이다.

SQL 한번으로 조회가 가능하다.

실행 결과를 보면, 연관된 엔티티(team) 객체도 프록시 객체가 아닌 실제 객체이다.

 

프록시와 즉시로딩 주의할 점

실무에서는 가급적 지연로딩 위주로 사용하자. 왜냐하면 예기치 못한 SQL이 발생한다.

즉시로딩은 JPQL에서 대표적으로 N+1 문제를 일으킨다.

 

지연로딩 활용

https://ict-nroo.tistory.com/132

  • Member와 Team을 자주 함께 사용한다 -> 즉시 로딩
  • Member와 Order는 가끔 사용한다 -> 지연 로딩
  • Order와 Product는 자주 함꼐 사용한다 -> 즉시 로딩
  • 위와 같이 설정해 놓고 쓸 수 있지만, 굉장히 이론적인 개념이고
  • 실무에서는 다 LAZY로 쓰자. 즉시 로딩 사용하지 말자.
  • JPQL fetch join이나, 엔티티 그래프 기능으로 해결하자.
  • 즉시 로딩은 상상하지 못한 쿼리가 나간다.

N+1 해결 방법

1.FETCH JOIN

가장 일반적인 방법이다. SQL 조인을 이용해서 연관된 엔티티를 함께 조회하므로 N+1 문제가 발생하지 않는다.

최초에 관련된 데이터를 한꺼번에 가져와서 객체화를 해줬기 때문에 DB 거치지 않고, 데이터 꺼내서 반환!


JPQL은 아래와 같다.

SELECT m FROM member m JOIN FETCH m.team

 

JOIN으로 같이 조회해서 Member 엔티티의 orders 속성에 초기화 하였기 때문에 더이상 N+1이 발생하지 않는다. (즉, 1개 쿼리로 문제 해결!)

 

2.하이버네이트 @BatchSize

하이버네이트가 제공하는 org.hibernate.annotations.BatchSize 어노테이션을 이용하면 연관된 엔티티를 조회할 때 지정된 size 만큼 SQL의 IN절을 사용해서 조회한다.

해당 size는 IN절에 올수있는 최대 인자 개수를 말한다. 만약 Member의 개수가 10개라면 위의 IN절이 2번 실행될것이다.

그리고 만약 지연로딩이라면 지연로딩된 엔티티 최초 사용시점에 5건을 미리 로딩해두고, 6번째 엔티티 사용 시점에 다음 SQL을 추가로 실행한다.

 

3.하이버네이트 @Fetch(FetchMode.SUBSELECT)

연관된 데이터를 조회할 때 서브쿼리를 사용해서 N+1 문제를 해결한다

즉시로딩으로 설정하면 조회시점에, 지연로딩으로 설정하면 지연로딩된 엔티티를 사용하는 시점에 위의 쿼리가 실행된다.


참고