
N+1은 리스트 쿼리 1번(+1) 실행 후,
리스트 안의 각 엔티티에서 LAZY 연관 필드를 접근할 때마다 N번의 추가 쿼리가 나가는 현상이다.
"부모 리스트 조회" 1번
"자식/연관 엔티티 로딩" N번
→ 총 1 + N(또는 1 + 2N, 1 + 3N ...) 쿼리가 발생한다.
N+1은 데이터 정합성을 깨는 동시성 문제라기보다, 요청당 DB 쿼리를 폭증시켜 지연/타임아웃을 유발하는 성능 문제다.
다만 지연이 커지면 사용자 재시도나 중복 요청을 유발해, 결과적으로 동시성 이슈가 더 잘 드러나는 환경을 만들 수 있다.
JPA에서 연관관계를 LAZY로 두면, 연관 엔티티는 처음 조회 시점에서는 실제로 가져오지 않고 프록시(대리 객체) 만 넣어둔다.
그리고 코드에서 연관 필드를 "진짜로" 접근하는 순간에,
그제서야 DB에 쿼리를 날려서 로딩한다.
문제는 "리스트 조회 + 루프/스트림에서 연관 접근" 조합이 너무 흔하다는 것.
예시)
N+1을 해결하는 핵심은 단순하다.
LAZY연관을 나중에 하나씩 가져오지 말고, 처음 조회할 때 필요한 연관을 같이 가져오면 된다.
이걸 대표적으로 해결하는 방법이 Fetch Join 과 @EntityGraph다.
fetch Joi은 JPQL에서 join fetch 를 직접 써서, 연관 엔티티를 무조건 같이 로딩하게 만드는 방식이다.
@Query("""
select r from LessonReservation r
join fetch r.timeSlot
join fetch r.lesson
where r.user.userId = :userId
""")
List<LessonReservation> findMyReservationsFetch(Long userId);
특징
@EntityGraph는 "이 메서드로 조회할 때는 이 연관들을 같이 가져와"를 Repository 메서드에 선언하는 방식이다.
@EntityGraph(attributePaths = {"timeSlot", "lesson"})
List<LessonReservation> findAllByUser_UserIdOrderByRequestedDtDesc(Long userId);
특징