[Spring / JPA] QueryDSL 개념 , 코드 적용해보기

minjonyyy·2025년 3월 14일

[Spring]

목록 보기
5/6
post-thumbnail

QueryDSL 이란?

: 다양한 데이터베이스 플랫폼에 접근하여 SQL과 유사한 문법으로 쿼리를 작성하여 데이터 처리를 수행하는 데 도움을 주는 프레임워크이다.

  • Entity의 매핑정보를 활용하여 쿼리에 적합하도록 쿼리 전용 클래스(Q클래스)로 재구성해주는 기술
  • QueryDSL은 ORM 환경에서 JPA 구현체인 Hibernate와 함께 사용되며 JPA와 서로 보완적인 관계로 활용될 수 있다.

Q-Class (메타 모델)

: 쿼리 타입 혹은 Q-Class 라는 이름으로 불리며, '도메인 모델의 구조'를 나타낸다.
주로 도메인 클래스에 대한 쿼리를 작성할 때 사용되며 도메인 클래스의 각 필드를 변수로 가짐

그래서 이게 뭔데?!?

-> QueryDSL에서 데이터베이스 테이블을 객체처럼 다룰 수 있도록 만들어진 QueryDSL 전용 클래스!
일반 Entity가 QueryDSL을 사용하기 위한 메타 모델로 만들어지는 클래스라고 생각하면 됨.

그래서 이걸 어떻게 만드는데?!???

-> 내가 직접 구현하지 않아도 된다.

  1. 컴파일 실행
    build.gradle 파일 내에 작성한 태스크가 수행됨

  2. 컴파일을 수행하는 과정에서 코드 생성 기구(Annotation Processing Tool)가 수행됨
    build.gradle에 작성한 QueryDSL의 annotation processor가 수행

  3. 컴파일이 완료되면 Q-Class가 자동 생성 끝!

아래에서 QueryDSL을 직접 코드에 적용해보며 보여주겠다.

QueryDSL을 사용하는 이유?

1. 문자가 아닌 자바 코드로 쿼리를 작성할 수 있어 컴파일 시점에 오류를 확인할 수 있다.

2. 복잡한 동적 쿼리를 쉽게 다룰 수 있다.

  • 기존 JPQL은 쿼리를 문자열로 작성하여 오타가 있거나 잘못 작성한다면 런타임 시점에만 오류 확인이 가능하다.
    하지만 QueryDSL은 컴파일 시점에 오류를 발견할 수 있고, 자바 코드를 사용하기 때문에 인텔리제이의 자동완성 기능까지 활용할 수 있다는 것이 큰 장점이다!

그럼 이제 코드에 적용해보자

1. build.gradle에 의존성 추가하기

dependencies {
    //QueryDSL 의존성 추가
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
  • build.gradle 의존성 추가 후에는 꼭 코끼리 버튼 눌러주기 ‼️

2. Q-Class 빌드 (intelliJ)



화면 오른쪽 Gradle 탭(코끼리 모양)의 Tasks에서
-> [build/clean]
-> [other/compileJava]
를 순서대로 실행시켜준다. (더블클릭!)


빌드가 끝난 후, build 디렉토리에 entity별 Q-Class가 자동생성된 것을 확인할 수 있다!👏🏻

3. JPAQueryFactory를 Bean으로 등록

@Configuration
public class JPAConfiguration {

    @PersistenceContext
    private EntityManager entityManager;

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

JPAQueryFactory 는 QueryDSL에서 제공하는 주요 클래스 중 하나이다.

4. QueryDSL 사용하기

기존 코드

public interface TodoRepository extends JpaRepository<Todo, Long>, TodoQueryRepository {

    @Query("SELECT t FROM Todo t " +
            "LEFT JOIN t.user " +
            "WHERE t.id = :todoId")
    Optional<Todo> findByIdWithUser(@Param("todoId") Long todoId);

}

기존 JpaRepository 를 사용한 findByIdWithUser 메서드이다.
JPQL로 간단하게 구현이 가능하지만, QueryDSL로 바꾸어 보겠다.

4-1. QueryDSL을 사용할 커스텀 Repository 만들기

JpaRepository 인터페이스에서는 QueryDSL을 직접 사용하지 않고,
별도의 새로운 인터페이스를 만들어서 QueryDSL을 적용할 메서드를 선언해주었다.

public interface TodoQueryRepository {

    Optional<Todo> findByIdWithUser(Long todoId);

}

아래는 실제 QueryDSL을 사용할 Repository 구현체이다.

@Repository
public class TodoQueryRepositoryImpl implements TodoQueryRepository{
    private final JPAQueryFactory jpaQueryFactory;

    public TodoQueryRepositoryImpl(JPAQueryFactory jpaQueryFactory) {
        this.jpaQueryFactory = jpaQueryFactory;
    }

    @Override
    public Optional<Todo> findByIdWithUser(Long todoId){

        Todo result = jpaQueryFactory
                .selectFrom(todo)
                .leftJoin(todo.user, user).fetchJoin()
                .where(todo.id.eq(todoId))
                .fetchOne();

        return Optional.ofNullable(result);

    }
}
  • selectfrom(todo)
    select(todo) / from(todo) 으로 나누어 작성해도 됨!

  • leftJoin(todo.user, user).fetchJoin()
    → Todo와 User를 LEFT JOIN하여 연관된 데이터를 한 번에 가져오기
    → N+1 문제를 방지하기 위한 fetchjoin()

  • where(todo.id.eq(todoId))
    → 특정 todoId에 해당하는 Todo를 필터링하기 (sql의 where절)

  • fetchOne()
    → 단일 객체 조회
    → 다건 조회 시 fetch() 사용

  • Optional.ofNullable(result)
    → 결과가 null일 수도 있으므로 Optional로 감싸서 반환

4-2. 서비스 로직에 적용

기존 TodoRepository에 QueryDSL을 사용한 커스텀 repository를 상속시켜준다.

public interface TodoRepository extends JpaRepository<Todo, Long>, TodoQueryRepository {

}
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()),
            todo.getCreatedAt(),
            todo.getModifiedAt()
        );
    }

이번 메서드는 간단한 쿼리였기 때문에 금방 구현이 끝났다.
하지만 더욱 복잡한 쿼리를 구현하게 된다면 QueryDSL이 좋은 이유를 체감하게 될 것이다.

💡 기존 JPQL 방식 vs QueryDSL 방식 비교

방식특징장점단점
JPQL (@Query)문자열 기반 쿼리간단한 조회 쿼리 작성 가능- 컴파일 타임 검증 없음
- 동적 쿼리 작성 어려움
QueryDSLJava 코드 기반 쿼리 DSL- 컴파일 타임 검증 가능
- 가독성 및 유지보수성 증가
- 동적 쿼리 작성 용이
초기 설정 필요 (Q-Class 생성 등)

0개의 댓글