Programming/Spring

JPA N+1 문제 정복하기: fetch join부터 batch size까지 실전 해결 전략

kwanghyun 2025. 7. 15. 17:59
반응형

JPA를 쓰다 보면 한 번쯤 겪게 되는 대표적인 성능 이슈, 바로 N+1 문제입니다.
이 글에서는 N+1 문제가 어떻게 발생하는지, 그리고 실제로 어떻게 해결할 수 있는지 정리해보았습니다.

1. N+1 문제란?

JPA에서는 연관 관계가 기본적으로 LAZY 로딩입니다.

@ManyToOne(fetch = FetchType.LAZY)
private Team team;

이 설정으로 인해, 연관된 엔티티는 실제로 접근하는 시점에 쿼리가 발생합니다.

예시:

List<Member> members = memberRepository.findAll();
for (Member m : members) {
    System.out.println(m.getTeam().getName()); // N개의 쿼리 발생!
}
  • Member 전체를 가져오는 쿼리 1번
  • Member가 참조하는 Team을 가져오는 쿼리 N번
  • → 총 N+1 쿼리 발생

2. 왜 문제가 되는가?

  • 단일 요청에 수십~수백 개의 쿼리가 발생할 수 있음
  • DB 커넥션 수, 네트워크 부하, 응답 속도 모두 저하
  • 특히 API 리스트 조회, 관리자 페이지, 검색 결과 등에서 치명적

3. 해결 방법 요약

방법 설명 장점 단점
fetch join JPQL에서 JOIN FETCH 사용 쿼리 1번으로 해결 페이징 불가, 중복 주의
@EntityGraph Repository 메서드에서 fetch join 지정 페이징 가능, 선언적 복잡한 경로 시 관리 어려움
batch size 설정 지연 로딩 대상들을 IN으로 묶어서 가져옴 설정만으로 해결 IN 조건이 커질 수 있음
DTO 분리 조회 연관 관계 무시하고 필요한 데이터만 추출 명확한 구조 코드 복잡도 증가

4. fetch join

예시

@Query("SELECT m FROM Member m JOIN FETCH m.team")
List<Member> findAllWithTeam();
  • Member와 Team을 한 번에 가져오는 쿼리
  • 매우 강력하지만 페이징과는 같이 쓸 수 없음

5. @EntityGraph

예시

@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
  • 내부적으로 fetch join 수행
  • 페이징과 함께 사용 가능
  • 단, 복잡한 경로를 여러 개 지정하는 경우 관리에 주의

6. batch size 설정

spring:
  jpa:
    properties:
      hibernate.default_batch_fetch_size: 100
  • LAZY 로딩 시 N개 객체를 한 쿼리로 IN 조건으로 묶어 가져옴
  • 쿼리 수는 줄이면서 LAZY 로딩 전략 유지 가능

7. DTO 프로젝션

  • 엔티티를 직접 로딩하지 않고, 원하는 필드만 DTO로 매핑
  • Querydsl, JPA Projections 등과 함께 사용

예시 (Querydsl)

queryFactory
    .select(new QMemberDto(member.id, member.name, team.name))
    .from(member)
    .join(member.team, team)
    .fetch();

8. 상황별 전략 선택

상황 추천 전략
상세 페이지 (단건 조회) fetch join
리스트 조회 + 페이징 EntityGraph, batch size
API 응답 최적화 DTO 분리 조회
복잡한 연관 조회 + 페이징 두 쿼리 전략 (count + fetch)

9. 마무리

  • N+1 문제는 성능 저하의 주요 원인이 될 수 있지만,
    다양한 해결책이 준비되어 있고, 각각의 상황에 맞는 전략을 선택하는 것이 중요합니다.
  • fetch join은 강력하지만, 무조건 사용하기보다는 상황에 따라 조절하는 것이 실무의 핵심입니다.

이제 N+1 문제를 만나도 당황하지 않고, 자신 있게 대응할 수 있을 거예요! 🚀


📌 다음 글: fetch join은 페이징이 정말 불가능할까?

N+1 문제를 해결하기 위해 자주 쓰이는 fetch join
그런데 이걸 페이징과 함께 쓰려고 하면 꼭 부딪히게 되는 말:

"fetch join은 페이징 불가능하다!"

하지만 정말 항상 그런 걸까요?

👉 다음 글에서 확인해보세요 → fetch join과 페이징, 진짜 안 되는 걸까?

반응형