QueryDSL
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은 복잡한 조건을 적용하고 동적 쿼리를 사용할 때 특히 유리합니다.
'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 |