Trouble Shooting : N+1 문제

mangez_js·2025년 3월 11일

Study

목록 보기
47/47

ORM(Object-Relational Mapping)에서 발생하는 성능 이슈 중 하나로, 특정 엔티티를 조회 할 때 연관된 엔티티들을 추가로 조회하면서 불필요한 쿼리가 다시 실행되는 문제

  • 하나의 Review엔티티를 가져올 때 관련된 Participants 엔티티가 존재한다면
  1. 1개의 쿼리로 Review 목록을 가져옵니다.
  2. 이후, N개의 쿼리로 각 Review와 연관된 Participants를 조회합니다.
  • 즉 하나의 Review를 가져오는데 추가적으로 Participants를 개별적으로 조회하게 되어 N+1개의 쿼리가 실행

문제 상황

  • review.getParticipants() 호출 시, 각 Participants를 개별적으로 조회
  • participant.getUsers().getId() 호출 시, 각 Users를 추가로 조회
  • 결과 : Review 1개 조회 시 N개의 추가 쿼리 발생 → N+1 문제 발생

원인 분석

  • Review 엔티티에서 Participants 엔티티를 조회할 때, Lazy Loading(지연 로딩) 설정으로 인해 개별 쿼리 실행
  • Participants에서 Users를 조회할 때도 각각 쿼리 실행 → 쿼리 다중 발생
  • 결과적으로 Review 1개 조회할 때 1개의 쿼리 + Participants 개수만큼 추가 쿼리 발생

해결방법

① 해결방법 : fetch join을 사용하여 한 번의 쿼리로 조회

@Query("SELECT r FROM Review r JOIN FETCH r.participants p JOIN FETCH p.users WHERE r.id = :reviewId")
Review findReviewWithParticipantsAndUsers(@Param("reviewId") Long reviewId);
  • JOIN FETCH를 사용하여 ReviewParticipants, Users를 한 번의 쿼리로 조회
  • N+1 문제 해결 → 쿼리 실행 횟수 1회로 줄어듦

② 해결방법 : @EntityGraph 활용

@EntityGraph(attributePaths = {"participants.users"})
@Query("SELECT r FROM Review r WHERE r.id = :reviewId")
Review findReviewWithParticipantsAndUsers(@Param("reviewId") Long reviewId);
  • @EntityGraph를 사용하여 필요한 연관 엔티티를 한 번에 가져옴
  • 코드 변경 없이 간단하게 fetch join과 유사한 효과

③ 해결방법 : @BatchSize 또는 hibernate.default_batch_fetch_size 설정

  • Lazy Loading 유지하면서도 여러 개 데이터를 한 번에 가져오도록 설정
  • 설정 방법 1(엔티티에 직접 적용)
@Entity
public class Participant {
    @ManyToOne(fetch = FetchType.LAZY)
    @BatchSize(size = 10)  // 10개씩 한 번에 조회
    private Users users;
}
  • 설정 방법 2(application.yml에서 설정)
spring:
  jpa:
    properties:
      hibernate.default_batch_fetch_size: 100

IN 쿼리를 활용하여 여러 개의 데이터를 한 번에 조회

  • 불필요한 개별 쿼리를 줄이고 성능 개선 가능

해결 방법 장단점

해결 방법주요 장점주요 단점
fetch join(JPQL 사용)한 번의 쿼리로 연관 엔티티를 모두 가져와서 성능 최적화JPQL을 직접 작성해야 하며, 복잡한 쿼리가 될 수 있음
@EntityGraph(어노테이션 기반)JPQL 수정 없이 적용 가능/ Lazy Loading 유지 가능/ 코드 재사용성이 높음Hibernate의 구현 방식에 따라 쿼리가 다르게 실행될 수도 있음
@BatchSize(IN 쿼리 사용)Lazy Loading을 유지하면서 개별 조회를 IN 쿼리로 최적화/ 설정으로 쉽게 작용 가능완벽한 N+1 해결책이 아님(여전히 여러 개의 쿼리가 발생)

결론

@EntityGraph를 적용하면 연관 엔티티를 미리 로딩하여 N+1 문제를 해결할 수 있으며, 전체적인 성능이 개선된다.
fetch join과 유사한 효과를 내면서도 JPQL을 수정하지 않아도 되므로 유지보수가 용이하다

0개의 댓글