반응형
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은 페이징 불가능하다!"
하지만 정말 항상 그런 걸까요?
반응형
'Programming > Spring' 카테고리의 다른 글
fetch join은 페이징 불가? (관계에 따른 진실과 Querydsl의 대응 전략) (0) | 2025.07.15 |
---|---|
Spring DI란? 주입 방식의 종류와 기본 개념 (0) | 2025.07.14 |
[Part 2] JPA 실전 설계 전략: DTO, Entity, VO, Projection 구분법 (1) | 2025.07.14 |
[Part 1] Spring 패키지 구조 설계 전략: MVC vs 도메인 중심 (0) | 2025.07.14 |
Spring 트랜잭션에서 `@Transactional`이 무시되는 이유와 주의할 점 (0) | 2025.07.14 |