기존 TodoService#getTodo(Long todoId) 메서드는 일정과 작성자 정보를 함께 조회하기 위해 JPQL 기반의 findByIdWithUser() 메서드를 사용하고 있었다.
@Query("SELECT t FROM Todo t JOIN FETCH t.user WHERE t.id = :id")
Optional<Todo> findByIdWithUser(@Param("id") Long id);
QueryDSL로 리팩토링할 때 단순히 selectFrom(todo).where(todo.id.eq(id))만 사용할 경우, 연관된 user 정보가 Lazy 로딩으로 인해 나중에 조회되면서 N+1 문제가 발생할 수 있음.
Todo todo = jpaQueryFactory
.selectFrom(QTodo.todo)
.where(QTodo.todo.id.eq(todoId))
.fetchOne();
→ 이후 todo.getUser()를 호출하면 쿼리가 한 번 더 나가며 N+1 문제 발생
JOIN FETCH를 QueryDSL로 대체하기 위해 fetchJoin()을 사용하여 아래와 같이 리팩토링함.
QTodo todo = QTodo.todo;
QUser user = QUser.user;
Todo result = jpaQueryFactory
.selectFrom(todo)
.join(todo.user, user).fetchJoin() // N+1 방지
.where(todo.id.eq(todoId))
.fetchOne();
QueryDSL에서 연관 관계의 지연 로딩 문제(N+1)를 해결하려면 반드시 fetchJoin()을 사용해야 한다.
JPQL의 JOIN FETCH는 QueryDSL에서는 join(...).fetchJoin()으로 대체할 수 있음.
단순 조회라고 해도 Entity 간 연관관계를 적극적으로 사용하는 경우에는 N+1 문제가 생길 수 있으므로 항상 주의해야 한다.