JPA를 실무에 도입하면 많은 개발자가 가장 먼저 마주하는 성능 문제 중 하나가 N+1 문제입니다. 간단한 데이터 조회 쿼리처럼 보이지만, 실제 서비스 환경에서는 심각한 성능 저하로 이어질 수 있습니다. N+1 문제의 발생 원인부터 실전 해결책인 Fetch Join
과 @EntityGraph
를 활용하는 을 정리했습니다.
1. N+1 문제란 무엇인가?
간단히 말해, 1번의 조회 쿼리(N) 이후에 관련된 엔티티를 조회하기 위한 추가 쿼리(+N)가 발생하는 문제입니다. 예를 들어 게시글(Post) 리스트를 조회하고, 각 게시글의 댓글(Comment) 리스트를 같이 가져오려고 할 때 다음과 같은 쿼리 구조가 됩니다.
List<Post> posts = postRepository.findAll();
for (Post post : posts) {
System.out.println(post.getComments().size());
}
위 코드의 결과는 다음과 같습니다:
- 1번 쿼리:
select * from post
- N번 쿼리:
select * from comment where post_id = ?
가 게시글 수만큼 반복
즉, 게시글이 100개라면 총 101개의 쿼리가 실행됩니다.
2. 왜 발생하는가?
기본적으로 JPA는 @OneToMany
등의 연관관계를 Lazy Loading으로 설정합니다. 따라서 해당 연관 필드에 실제 접근이 일어나기 전까지는 데이터를 로딩하지 않죠. 하지만 루프 안에서 호출되면 그때마다 SQL 쿼리가 발생하게 됩니다.
3. 해결 방법 1: Fetch Join
기본 사용법
@Query("SELECT p FROM Post p JOIN FETCH p.comments")
List<Post> findAllWithComments();
JOIN FETCH
를 사용하면 한 번의 쿼리로 연관 엔티티를 즉시 로딩할 수 있습니다. 다만 1:N 관계에서는 중복 row가 생기므로 distinct
키워드를 함께 사용하거나 애플리케이션에서 중복 제거가 필요합니다.
@Query("SELECT DISTINCT p FROM Post p JOIN FETCH p.comments")
List<Post> findAllWithCommentsDistinct();
주의 사항
JOIN FETCH
는 2개 이상의 연관관계를 동시에 가져올 경우 조인 결과가 폭발할 수 있음- 페이징 처리 시 fetch join은 부적절 (Spring Data JPA에서 예외 발생 가능)
4. 해결 방법 2: @EntityGraph
@EntityGraph
는 Fetch 전략을 메서드 단위에서 명시적으로 조정할 수 있게 해줍니다.
@EntityGraph(attributePaths = "comments")
List<Post> findAll();
JPA 구현체가 내부적으로 fetch join과 유사하게 처리하지만, JPQL 작성 없이도 fetch 적용이 가능하다는 장점이 있습니다.
다중 연관 필드 로딩
@EntityGraph(attributePaths = {"comments", "author"})
List<Post> findByTitleContaining(String keyword);
페이징과 함께 사용 가능
@EntityGraph
는 Pageable
과도 잘 호환되므로 페이징 처리가 필요한 경우 매우 유용합니다.
5. 정리: 언제 어떤 방법을 쓸까?
조건 | 추천 방법 |
---|---|
단건 조회 + 연관 관계 필요 | Fetch Join |
다건 조회 + 페이징 필요 | @EntityGraph |
1:N 관계의 리스트 반환 | Fetch Join + DISTINCT 처리 필요 |
JPA Query 없이 사용 | @EntityGraph |
마무리
N+1 문제는 초반에는 간과되기 쉽지만, 트래픽이 많아질수록 DB 부하와 응답 지연으로 이어지는 치명적인 병목 요소입니다. JOIN FETCH
와 @EntityGraph
는 각각 장단점이 있으므로 상황에 맞게 선택해야 하며, 무조건 EAGER 전략을 쓰는 것은 권장드리고 싶지 않습니다.
'개발 > JAVA' 카테고리의 다른 글
[JAVA] Spring Boot와 MySQL 연동하기 - 입문 가이드 (0) | 2025.10.17 |
---|---|
[JAVA] Spring에서 트랜잭션 관리하기 – @Transactional 완벽 이해 (0) | 2025.10.16 |
[JAVA] JPA Fetch 전략 완벽 가이드 – Lazy vs Eager 실무 적용법 (0) | 2025.10.14 |
[JAVA] Entity 연관관계 완전 정복 – 1:N, N:M, Cascade까지 실무 중심 정리 (0) | 2025.10.13 |
[JAVA] JPQL과 @Query 사용법 – Spring Data JPA에서 복잡한 쿼리 다루기 (0) | 2025.10.12 |