ORM(Object-Relational Mapping)에서 발생하는 성능 이슈 중 하나로, 특정 엔티티를 조회 할 때 연관된 엔티티들을 추가로 조회하면서 불필요한 쿼리가 다시 실행되는 문제
- 하나의
Review엔티티를 가져올 때 관련된Participants엔티티가 존재한다면
- 1개의 쿼리로
Review목록을 가져옵니다.- 이후, N개의 쿼리로 각
Review와 연관된Participants를 조회합니다.
- 즉 하나의
Review를 가져오는데 추가적으로Participants를 개별적으로 조회하게 되어N+1개의 쿼리가 실행

review.getParticipants()호출 시, 각Participants를 개별적으로 조회participant.getUsers().getId()호출 시, 각Users를 추가로 조회- 결과 :
Review1개 조회 시 N개의 추가 쿼리 발생 → N+1 문제 발생
Review엔티티에서Participants엔티티를 조회할 때, Lazy Loading(지연 로딩) 설정으로 인해 개별 쿼리 실행Participants에서Users를 조회할 때도 각각 쿼리 실행 → 쿼리 다중 발생- 결과적으로
Review1개 조회할 때 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를 사용하여Review와Participants,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 설정@Entity
public class Participant {
@ManyToOne(fetch = FetchType.LAZY)
@BatchSize(size = 10) // 10개씩 한 번에 조회
private Users users;
}
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을 수정하지 않아도 되므로 유지보수가 용이하다