[JPA] JPQL을 QueryDSL로 리팩토링하기!
2025. 5. 10. 01:16

요구사항

  • JPQL로 작성된 `findByIdWithUser`를 QueryDSL로 변경하기
  • N+1 문제가 발생하지 않도록 유의하기

1. 빌드 환경 구성

  • build.gradle에 해당 코드 추가 (`build.gradle`)
  • Q파일 생성 경로 지정 (`build.gradle`)
dependencies {
    // queryDSL 설정
    implementation "com.querydsl:querydsl-jpa:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
    implementation "com.querydsl:querydsl-core"
    implementation "com.querydsl:querydsl-collections"
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" // querydsl JPAAnnotationProcessor 사용 지정
    annotationProcessor "jakarta.annotation:jakarta.annotation-api" // java.lang.NoClassDefFoundError (javax.annotation.Generated) 대응 코드
    annotationProcessor "jakarta.persistence:jakarta.persistence-api" // java.lang.NoClassDefFoundError (javax.annotation.Entity) 대응 코드
}
// QueryDSL 설정

// QClass 파일이 기본 경로(src/main/generated)가 아닌 다른 위치에 생성될 경우,
// IntelliJ가 빌드된 클래스 파일을 중복으로 스캔하면서 충돌이 발생할 수 있으므로
// QClass 생성 위치를 명시적으로 지정해 충돌 방지
def generated = 'src/main/generated'

// QueryDSL의 QClass 생성 경로 지정
tasks.withType(JavaCompile) {
    options.getGeneratedSourceOutputDirectory().set(file(generated))
}

// 생성된 QClass 경로를 Java 소스 경로에 추가하여 인식 가능하게 함
sourceSets {
    main.java.srcDirs += [ generated ]
}

// gradle clean 시 생성된 QClass 디렉토리도 함께 삭제
clean {
    delete file(generated)
}
  • Gradle Wrapper 수동 설치
  • 🌟QuertdslConfig 파일 생성 
    • `JPAQueryFactory `는 QueryDSL의 쿼리 실행 엔진.
    • 이 코드를 통해 Spring이 `JPAQueryFactory`를 스프링빈으로 등록함
    • 이거 없으면 TodoRepository에서 `JPAQueryFactory`를 생성자 주입 못받아서 오류 발생
      ( No qualifying bean of type JPAQueryFactor )
@Configuration
public class QuerydslConfig {

    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

2. QueryDSL로 전환

  • 기존 JPQL 방식 코드 수정
    • `findByIdWithUser()`는 커스텀인터페이스에서 이미 정의되었기 때문에 여기에서 선언하지 않고도 사용 가능(`extends`)
    • JPQL로 작성된 메서드를 제거하면 QueryDSL 방식으로 완전히 바뀌어서 n+1문제 방지할 수 있음
// 기본 CRUD용 repository
public interface TodoRepository extends JpaRepository<Todo, Long>, TodoRepositoryCustom {

//    @Query("SELECT t FROM Todo t " +
//            "LEFT JOIN t.user " +
//            "WHERE t.id = :todoId")
//    Optional<Todo> findByIdWithUser(@Param("todoId") Long todoId);
}
  • QueryDSL 방식 코드 작성
// 사용자 정의 인터페이스 : 동적 검색용 로직
public interface TodoRepositoryCustom {
   Optional<Todo> findByIdWithUser(Long todoId); // QueryDSL 구현
}
// Custom에 선언된 메서드를 JPQL로 직접 구현
@Repository
@RequiredArgsConstructor  // queryFactory가 생성자로 주입됨
public class TodoRepositoryImpl implements TodoRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    // QueryDSL 구현
    @Override
    public Optional<Todo> findByIdWithUser(Long todoId) {
    // todoId로 특정todo 조회할때 연결된 User정보까지 같이 조회
    
        // Q클래스 : QueryDSL이 엔티티를 바탕으로 자동 생성한 쿼리전용클래스 
        QTodo todo = QTodo.todo;
        QUser user = QUser.user;

        Todo result = queryFactory
            .selectFrom(todo)
            // todo조회할거야
            .leftJoin(todo.user, user).fetchJoin()
            // todo의 user필드 기준으로 user테이블과 leftjoin
            // fetchjoin : 연관된 user객체도 한번에 가져와(n+1 방지)
            .where(todo.id.eq(todoId))
            // todo.id와 입력받은 todoid가 같을때
            .fetchOne();
            // 결과 1건 가져와

        return Optional.ofNullable(result);
        // 결과가 null일수도 있으니까 Optional로 감싸서 반환
    }
}

🌕🌕🌕 하루 회고

JPQL과 QueryDSL의 개념에 대해서는 배웠었는데 직접 코드를 리팩토링하는건 처음이였다. 단순히 문자열 쿼리만 바꿔적어주면 될 줄 알았는데 생각보다 환경설정이 복잡했다. gradle/wrapper 오류는 나만 나는건지, 모두가 나는건지 모르겠다. 그래도 환경 설정은 한번 잘 해두면 앞으로 안해도 되는거니까 잘 마쳤다. 내가 느낀 리팩토링 포인트는 QuerydslConfig에서 JPAQueryFactory를 빈으로 등록해줘야 생성자 주입이 가능하다는 점! 따로 QuerydslConfig 파일을 안 만들었더니 자꾸 생성자 주입이 안된다는 오류가 나서 QueryDSL은 어디서 생성자 주입을 해주어야하나 한참을 구글링했다.. 이부분을 놓쳐서 한번 오류가 났지만, 해결하고나니까 오히려 흐름을 이해하는데 도움이 된 듯 하다. 일단은 간단한 메서드 하나만 전환하는 식으로 리팩토링 해보았는데, 문자열 대신에 객체로 쿼리를 짠다는 점에서 만족스럽다. 

LIST