che01 님의 블로그
QueryDSL leftJoin에서 fetchJoin 없이도 N+1 문제가 발생하지 않는 경우 본문
결론
QueryDSL에서 leftJoin()을 사용할 때 항상 fetchJoin()이 필요한 것은 아니다. DTO 프로젝션으로 조회하는 경우에는 N+1 문제가 발생하지 않는다.
상황별 N+1 문제 발생 여부
1. 엔티티 직접 조회 - N+1 발생함
List<Todo> todos = queryFactory
.selectFrom(todo)
.leftJoin(todo.managers, manager) // fetchJoin 없음
.fetch();
// 연관 객체 접근 시 N+1 발생
for (Todo t : todos) {
t.getManagers().size(); // 각 Todo마다 추가 쿼리 실행
}
2. DTO 프로젝션 조회 - N+1 발생하지 않음
List<TodoSearchResponse> result = queryFactory
.select(Projections.fields(
TodoSearchResponse.class,
todo.title.as("title"),
manager.countDistinct().as("managerCount"),
comment.countDistinct().as("commentCount")
))
.from(todo)
.leftJoin(todo.managers, manager) // fetchJoin 없어도 됨
.leftJoin(todo.comments, comment) // fetchJoin 없어도 됨
.fetch();
왜 DTO 프로젝션에서는 N+1이 발생하지 않는가?
- 엔티티를 영속성 컨텍스트에 올리지 않음: DTO는 단순한 데이터 전송 객체로, JPA가 관리하지 않는다.
- Lazy Loading이 작동하지 않음: 엔티티가 아니므로 연관 객체에 대한 프록시 생성이나 지연 로딩이 일어나지 않는다.
- 필요한 데이터만 한 번에 조회: SELECT 절에 명시된 필드들만 조인을 통해 한 번의 쿼리로 가져온다.
fetchJoin이 필요한 경우
엔티티 조회 시 연관 객체도 함께 사용하는 경우
List<Todo> todos = queryFactory
.selectFrom(todo)
.leftJoin(todo.managers, manager).fetchJoin() // fetchJoin 필요
.fetch();
// 이제 N+1 없이 연관 객체 사용 가능
for (Todo t : todos) {
t.getManagers().size(); // 추가 쿼리 없음
}
DTO에서 연관 객체의 내부 필드에 접근하는 경우
.select(Projections.fields(
TodoSearchResponse.class,
todo.title.as("title"),
manager.user.nickname.as("managerNickname") // 연관 객체의 내부 필드
))
.from(todo)
.leftJoin(todo.managers, manager).fetchJoin() // fetchJoin 필요
.leftJoin(manager.user, user).fetchJoin() // 추가 fetchJoin 필요
정리
- DTO 프로젝션 + 집계 함수: fetchJoin 불필요
- 엔티티 조회 + 연관 객체 사용: fetchJoin 필수
- DTO 프로젝션 + 연관 객체 내부 필드: fetchJoin 필요
성능 최적화를 위해서는 용도에 맞는 조회 방식을 선택하는 것이 중요하다.