본문 바로가기

Spring

Spring - QueryDSL

QueryDSL

<a href="https://www.flaticon.com/kr/free-icons/sql" title="sql 아이콘">Sql 아이콘 제작자: Freepik - Flaticon</a>

QueryDSL은 타입 안전성과 코드 가독성을 제공하여 JPA와 함께 사용할 수 있는 강력한 쿼리 빌더 라이브러리입니다. 이를 통해 SQL, JPQL, 그리고 HQL 같은 쿼리를 타입 안전하게 작성할 수 있습니다. 자세히 알아보겠습니다.

 

QueryDSL 을 말하기전에 우선적으로 알아야 될 것들이 있습니다. Spring framework 에서는 DB 에 데이터를 조회하는 방식으로 JDBC 부터 시작하여, `Mybatis` 나 `JPA` 같은 ORM 등을 추가적으로 개발하였고, `JPA` 에서 좀 더 확장하여 `JPQL` 과 `QueryDSL` 이라는 라이브러리까지 개발하게 되었습니다. 사실 이렇게 개발된 순서에 대한 내용은 확실하지 않으나, 라이브러리를 사용하는 방식을 보면 어떤식으로 편의성이 개선되고 업그레이드 되어왔는지 이해할 수 있던 것 같습니다. 이번 포스팅에서는 QueryDSL 에 대해서만 다룰 예정이나, 추후에 `JDBC` 와 `JPA`, `JPQL`, `QueryDSL` 을 연관지어 전체적인 흐름에 대해서 알아보도록 하겠습니다.

 

1. QueryDSL의 장점

  • 타입 안전성: QueryDSL은 코드 작성 시점에 타입 검사를 제공하여, 잘못된 쿼리로 인한 오류를 컴파일 시점에 잡아낼 수 있습니다. 이는 JPQL이나 SQL처럼 문자열을 사용하는 방식보다 안전하며, 런타임 오류를 줄여줍니다.
  • 가독성: SQL이나 JPQL 문자열을 사용하는 대신, 자바 메서드 체이닝 방식으로 쿼리를 작성합니다. 덕분에 코드의 가독성이 높아지고, 리팩토링이 용이합니다.
  • 유연성: 복잡한 조건문, 동적 쿼리, 페이징 처리 등을 구현할 때 유용합니다.
  • 재사용 가능성: 쿼리를 객체화하여 쉽게 재사용할 수 있습니다.

 

 

2. QueryDSL의 구동 방식

QueryDSL은 데이터베이스 엔티티 클래스를 기반으로 “Q타입”이라는 동적 쿼리 객체를 생성합니다. 이 Q타입을 통해 엔티티의 필드에 접근하여 쿼리를 생성하게 됩니다. Spring과의 연동이 가능하며, QueryDSL은 JPAQueryFactory를 통해 쿼리를 빌드하고 실행하는 방식으로 동작합니다.

 

 

 

3. QueryDSL의 사용법 및 구조

3.1 Q타입과 Q클래스

QueryDSL을 사용할 때, 엔티티 클래스마다 `Q타입`이라 불리는 클래스를 생성하게 됩니다. 이 Q타입 클래스는 엔티티의 각 필드에 대한 동적 쿼리를 쉽게 작성할 수 있도록 지원합니다. 예를 들어, `Todo`라는 엔티티가 있다면 `QTodo`라는 이름의 클래스가 생성됩니다. 이를 통해 `QTodo.todo.title` 처럼 엔티티의 필드를 참조하여 조건을 설정할 수 있습니다.

 

Q클래스 예시:

 

1. `QTodo` 클래스는 `title`, `user`, `createdAt`과 같은 `Todo` 엔티티의 필드를 포함하며, 쿼리 작성 시 타입 안전성을 제공합니다.

2. 기본적으로 `Q타입` 클래스는 `target/generated-sources` 폴더에 생성됩니다.

 

3.2 JPQL과 QueryDSL의 차이

  • JPQL: JPA에서 제공하는 객체 지향 쿼리 언어로, 쿼리를 문자열로 작성해야 합니다.
  • QueryDSL: 메서드 체인 방식으로 타입 안전한 쿼리를 작성합니다. QueryDSL은 select, from, where 등의 메서드를 제공하여 SQL 스타일로 쿼리를 작성하지만, JPQL이나 SQL과 다르게 문자열이 아닌 객체 기반으로 작성합니다.

3.3 QueryFactory와 쿼리 빌드

  • JPAQueryFactory: QueryDSL 쿼리를 작성하기 위한 팩토리 클래스입니다. 보통 Spring에서 Bean으로 등록하여 사용합니다. 주입받은 EntityManager를 통해 인스턴스가 생성됩니다.
  • 쿼리 작성 시 jpaQueryFactory를 통해 select, from, where 등을 호출하여 쿼리를 빌드합니다.

 

 

4. QueryDSL Custom 클래스 구현

QueryDSL을 통해 커스텀 쿼리를 작성하려면 Custom Repository 인터페이스와 그 구현체가 필요합니다.

4.1 Custom Repository 인터페이스 생성

1. 기존 리포지토리 인터페이스에 커스텀 메서드를 정의할 수 있습니다.

2. 예를 들어, `TodoRepositoryCustom` 인터페이스를 생성하여 메서드를 선언합니다.

public interface TodoRepositoryCustom {
    Page<TodoSummaryResponse> findTodosByKeywords(String titleKeyword, String nicknameKeyword, LocalDateTime startDate, LocalDateTime endDate, Pageable pageable);
}

 

4.2 Custom Repository 구현체

인터페이스에서 정의한 메서드를 구현하려면 `TodoRepositoryCustomImpl` 이라는 클래스를 작성하여 실제 쿼리 로직을 넣습니다. 이 클래스는 `TodoRepositoryCustom` 을 구현하고, `JPAQueryFactory`를 주입받아 QueryDSL 쿼리를 작성합니다.

@RequiredArgsConstructor
public class TodoRepositoryCustomImpl implements TodoRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    @Override
    public Page<TodoSummaryResponse> findTodosByKeywords(
            String titleKeyword, String nicknameKeyword, LocalDateTime startDate, LocalDateTime endDate, Pageable pageable) {

        QTodo todo = QTodo.todo;
        QManager manager = QManager.manager;
        QUser user = QUser.user;

        // QueryDSL을 사용한 쿼리 작성
        JPQLQuery<TodoSummaryResponse> query = queryFactory
            .select(Projections.constructor(
                TodoSummaryResponse.class,
                todo.title,
                todo.managers.size().castToNum(Long.class),
                todo.comments.size().castToNum(Long.class)
            ))
            .from(todo)
            .leftJoin(todo.managers, manager)
            .leftJoin(manager.user, user)
            .where(
                titleContains(titleKeyword),
                managerNicknameContains(nicknameKeyword),
                createdDateBetween(startDate, endDate)
            )
            .groupBy(todo.id)
            .orderBy(todo.createdAt.desc());

        long total = query.fetchCount();
        List<TodoSummaryResponse> results = query
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .fetch();

        return new PageImpl<>(results, pageable, total);
    }

    private BooleanExpression titleContains(String titleKeyword) {
        return titleKeyword != null ? QTodo.todo.title.contains(titleKeyword) : null;
    }

    private BooleanExpression managerNicknameContains(String nicknameKeyword) {
        return nicknameKeyword != null ? QUser.user.nickname.contains(nicknameKeyword) : null;
    }

    private BooleanExpression createdDateBetween(LocalDateTime startDate, LocalDateTime endDate) {
        return QTodo.todo.createdAt.between(startDate, endDate);
    }
}

 

 

5. QueryDSL을 사용한 쿼리 작성 방식

위의 TodoRepositoryCustomImpl 클래스에서 다음과 같은 방식으로 QueryDSL 쿼리를 작성했습니다:

  • JPAQueryFactory를 통해 쿼리를 시작합니다.
  • select 절에 반환할 필드를 정의합니다. 여기서는 Projections.constructor를 사용하여 TodoSummaryResponse 객체에 필요한 필드를 선택했습니다.
  • from 절에서 기준이 되는 엔티티(todo)를 설정하고 필요한 경우 leftJoin으로 연관된 엔티티를 조인합니다.
  • 조건이 필요한 경우 where 절에서 BooleanExpression으로 작성합니다.
  • groupBy와 orderBy로 그룹화와 정렬을 설정합니다.
  • offset, limit을 통해 페이지네이션을 적용하고 fetch()로 결과를 가져옵니다.

이 방식으로 QueryDSL을 사용하면 문자열을 사용하는 JPQL보다 실수할 가능성이 줄어들고, 코드 가독성도 높아집니다. QueryDSL은 복잡한 조건을 적용하고 동적 쿼리를 사용할 때 특히 유리합니다.

728x90
반응형

'Spring' 카테고리의 다른 글

Spring - AWS  (1) 2024.11.17
Spring - DB 와의 상호작용  (1) 2024.11.16
Spring - Spring Security  (0) 2024.11.13
Spring - JDBC Template  (1) 2024.11.10
Spring - Database Driver  (1) 2024.11.09