이번 작업에서는 JPQL로 작성된 쿼리를 QueryDSL로 변경하여 동적 쿼리 작성과 N+1 문제 해결을 목표로 했습니다. QueryDSL은 직관적인 메서드 체인을 통해 동적 쿼리를 작성할 수 있는 라이브러리로, 엔티티 간의 조인을 명확하게 설정하여 JPA를 활용한 쿼리 성능을 개선할 수 있습니다.
의존성 추가 및 Q 클래스 생성
build.gradle
파일에 QueryDSL 의존성을 추가합니다.
dependencies {
implementation 'com.querydsl:querydsl-jpa'
implementation 'com.querydsl:querydsl-apt'
}
configurations {
querydsl.extendsFrom compileClasspath
}
def querydslDir = "$buildDir/generated/querydsl"
sourceSets {
main.java.srcDir querydslDir
}
compileQuerydsl {
options.annotationProcessorPath = configurations.querydsl
}
tasks.withType(JavaCompile) {
options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
}
이후, ./gradlew compileQuerydsl
명령어를 실행하여 각 엔티티의 Q 클래스를 생성합니다. 예를 들어 Todo
엔티티가 있다면, QueryDSL이 QTodo
라는 이름의 클래스를 자동 생성합니다. 이 클래스는 QueryDSL에서 Todo
필드에 접근할 수 있는 객체입니다.
JPAQueryFactory
빈 설정
JPAQueryFactory
빈을 등록해야 합니다. 이를 위해 JPAConfiguration
이라는 설정 클래스를 생성합니다.@Configuration
public class JPAConfiguration {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
JPAQueryFactory
가 빈으로 등록되어, QueryDSL을 통해 JPA의 동적 쿼리를 작성할 수 있게 됩니다.Custom Repository 생성
QueryDSL을 적용할 메서드를 Repository
에 추가하려면, Custom Repository와 Implementation 클래스를 생성해야 합니다.
Custom Repository Interface (TodoRepositoryCustom
):
public interface TodoRepositoryCustom {
Optional<Todo> findByIdWithUser(Long todoId);
}
Custom Repository Implementation (TodoRepositoryCustomImpl
):
@Repository
public class TodoRepositoryCustomImpl implements TodoRepositoryCustom {
private final JPAQueryFactory queryFactory;
public TodoRepositoryCustomImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
@Override
public Optional<Todo> findByIdWithUser(Long todoId) {
QTodo todo = QTodo.todo;
QUser user = QUser.user;
Todo result = queryFactory.selectFrom(todo)
.leftJoin(todo.user, user).fetchJoin()
.where(todo.id.eq(todoId))
.fetchOne();
return Optional.ofNullable(result);
}
}
QTodo
와 QUser
는 QueryDSL이 자동 생성한 클래스입니다. 이를 통해 Todo
와 User
엔티티의 필드에 접근할 수 있습니다.leftJoin(todo.user, user).fetchJoin()
을 통해 Todo
와 User
엔티티를 조인하여 N+1 문제를 방지하고 한 번의 쿼리로 데이터를 가져옵니다.fetchOne()
을 사용하여 단일 결과를 가져오도록 구현했습니다.Main Repository에 Custom Repository 확장
TodoRepository
가 TodoRepositoryCustom
을 상속하도록 하여 QueryDSL 메서드를 사용할 수 있게 설정합니다.public interface TodoRepository extends JpaRepository<Todo, Long>, TodoRepositoryCustom {
// JpaRepository의 기본 메서드 외에 QueryDSL 메서드 사용 가능
}
Service에서 QueryDSL 메서드 사용
이제 TodoService
에서 QueryDSL로 구현한 findByIdWithUser
메서드를 사용할 수 있습니다.
@Service
public class TodoService {
private final TodoRepository todoRepository;
@Autowired
public TodoService(TodoRepository todoRepository) {
this.todoRepository = todoRepository;
}
public TodoResponse getTodo(long todoId) {
Todo todo = todoRepository.findByIdWithUser(todoId)
.orElseThrow(() -> new InvalidRequestException("Todo not found"));
User user = todo.getUser();
return new TodoResponse(
todo.getId(),
todo.getTitle(),
todo.getContents(),
todo.getWeather(),
new UserResponse(user.getId(), user.getEmail(), user.getNickname()),
todo.getCreatedAt(),
todo.getModifiedAt()
);
}
}
JPAQueryFactory
빈을 설정하는 과정이 필요했습니다.leftJoin(...).fetchJoin()
으로 연관 엔티티 데이터를 한 번의 쿼리로 가져오며, N+1 문제를 방지할 수 있었습니다.처음 QueryDSL을 적용하는 과정이 어려웠지만, 앞으로 더 복잡한 동적 쿼리를 쉽게 구현할 수 있는 좋은 방법이라는 점을 느꼈습니다. JPA와 QueryDSL을 함께 사용하면 쿼리 작성이 더 유연해지고 성능 최적화도 가능해지므로, 더 다양한 상황에서 활용해볼 수 있을 것 같습니다.