JPA - N+1 문제

데일리·2024년 11월 20일
1

TIL

목록 보기
10/16
post-thumbnail

우리가 흔히 사용하고 있는 라이브러리인 JPA에는 조심해야할 문제가 하나 있다. 바로 N+1문제 웬만한 실무를 다뤄본 사람들은 알고 있고 이를 조심하려고 하지만 한번 씩은 겪게되고 서버가 터지는 문제를 맞닦드리게 된다.... 오늘은 이 N+1문제에 대해 알아보자

N+1문제란?

우선 n+1문제가 발생하는 상황부터 보면 이해가 빠르다.

  • 1번의 쿼리로 엔티티 리스트를 조회한다(예: 부모 엔티티 조회)
  • 이후 각 엔티티에 연관된 n개의 하위 엔티티를 조회한다.

이렇게 되면 이제 하위 엔티티를 조회하기 위해 N번의 추가 쿼리가 발생되버린다.

예시를 보자
1. Select * from parent
-> 부모 엔티티 10개를 가져온다
2. 각각의 부모에 대해 연관된 자식 엔티티를 조회

select * from child where parent_id = 1;
select * from child where parent_id = 2;
...
select * from child where parent_id = 10;

위의 예시처럼 부모의 자식들을 조회할 때 마다 우리는 위의 쿼리가 발생하기 때문에 성능저하 및 DB에 큰 부하를 줄 수 있다.

해결 방법

  1. 즉시 로딩(Eager Loading)을 지양하고 지연 로딩(Lazy Loading)을 사용
  • @OneToMany, @ManyToOne 등의 관계에서 fetch = FetchType.LAZY를 설정하여 기본적으로 연관된 엔티티를 가지고 오지 않게끔 하는 것이다.

음,,,, 근데 지연로딩을 하더라도 하위 엔티티를 조회하는 순간 n+1 문제가 발생하지 않나요???

정확하다! 지연문제 만으로는 N+1을 완벽하게 방지할 수는 없다. 지연로딩은 기존 쿼리를 통해 N+1을 막을 뿐 해당 쿼리로 하위 엔티티를 조회하는 순간 다시 쿼리가 나가기 때문에 N+1을 방지하기 위해서는 아래와 같은 방법 또한 고려해야한다.

  1. Join fetch를 사용
  • JPQL에서 join fetch를 사용해 연관된 데이터를 한번의 쿼리로 가져온다.
@Query("SELECT p FROM Parent p JOIN FETCH p.children")
List<Parent> findAllWithChildren();

다만 위의 방법을 과도하게 이용할 경우 DB 성능에 부하가 생길 수 있기 때문에 쿼리를 신중하게 짜야하고 그에 따른 인덱스 생성 또한 필수이다.

  1. EntityGraph 사용
  • JPA에서 제공하는 EntityGraph를 사용해 필요한 연관 데이터를 한번에 조회
@EntityGraph(attributePaths = {"children"})
@Query("SELECT p FROM Parent p")
List<Parent> findAllWithEntityGraph();
  1. Batch size 설정
  • hibernate에서 @BatchSize 또는 JPA의 글로벌 설정을 이용하여 연관 엔티티를 해당 배치 사이즈만큼 일괄로 가져온다.
@BatchSize(size = 10)
@OneToMany(mappedBy = "parent")
private List<Child> children;
  1. 쿼리 최적화
  • 필요한 데이터만 직접 쿼리하여 N+1 문제를 방지한다.
@Query("SELECT p FROM Parent p LEFT JOIN FETCH p.children")
List<Parent> findParentsWithChildren();

마무리

위의 여러가지 방식이 있지만 현재 우리 회사에서는 2번인 fetch join과 5번인 쿼리 최적화를 자주 사용한다. n+1문제는 코드 짤때는 인지하기가 어렵고 막상 배포를 했을 때 DB I/O의 성능을 보고 확인하기 때문에 처음 쿼리를 짤 때 주의를 기울여 짜는 것이 중요하다. 그럼 오늘의 포스팅 마무리~

profile
하루에 한편 씩 읽기 좋은 테크 로그

0개의 댓글