QueryDSL의 특징
QueryDSL은 Java에서 안전하고 가독성 있는 쿼리를 작성할 수 있게 해주는 프레임워크이다.
- 타입 안전성: 컴파일 시점에 쿼리의 타입을 검증하여 런타임 에러 방지
- 가독성: 코드 형태로 쿼리를 작성하므로 가독성이 높아지고 유지보수가 용이하다
- 동적 쿼리 지원: 조건에 따른 동적 쿼리를 유연하게 생성할 수 있다.
- 통합성: JPA, Hibernate, SQL과 쉽게 통합 가능하다.
특히 Spring Data JPA에서 기본적으로 제공하는 인터페이스인 JpaRepository와 비교해보자.
JpaRepository는 메서드 이름으로 쿼리를 생성할 수 있어 간단하지만, 동적 쿼리나 Join 같이 복잡한 내용이 쿼리에 포함되기 시작하면 매우 골치아프다. 따라서 간단한 CRUD 작업은 JpaRepository를 사용하고, QueryDSL로 복잡한 조건이 포함된 동적 쿼리를 사용하고 있다.
여기서 QueryDSL을 사용하면 JpaRepository의 주요 특징인 영속성 컨텍스트를 활용하여 1차 캐시 및 변경 감지 기능을 잃는 것 아니냐 할 수 있다.
하지만 QueryDSL 또한 JPA와 통합되어 동작하기 때문에 영속성 컨텍스트를 동일하게 사용할 수 있다.
QueryDSL로 작성된 쿼리가 실행되면 JPA의 EntityManager는 먼저 영속성 컨텍스트의 캐시에서 데이터를 조회한다.
또한 조회한 엔티티가 변경되면 변경사항이 자동으로 감지되어 데이터베이스에 반영된다.
따라서 QueryDSL은 JPA의 핵심기능을 그대로 사용하면서도 복잡한 동적 쿼리를 설계하는데 굉장히 편리하다.
QueryDSL 설정 방법
//QueryDsl
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
build.gradle에 위 내용을 추가하고 build 한다.
build가 성공적으로 완료되면 다음과 같이 build/generated 하위에 엔티티 클래스에 대한 Q 클래스가 생성된다.
이후에도 기술하겠지만 해당 Q 클래스를 통해 QueryDSL에서 편리하게 query를 작성할 수 있다.
이후 'config/QueryDSLConfig.java' 컨피그 파일을 생성하여 아래 내용을 추가해 JPAQueryFactory를 Spring Bean으로 등록한다.
@Configuration
@RequiredArgsConstructor
public class QueryDSLConfig {
private final EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
QueryDSL 쿼리 생성 방법
QueryDSL로 쿼리를 작성하기 위해서는 Q 클래스를 이용한다고 언급했다.
import static app.ebel.steadybucks.entity.QClan.clan;
import static app.ebel.steadybucks.entity.QUser.user;
import static app.ebel.steadybucks.entity.QUserClan.userClan;
@Override
public UserClan findUserClanByIds(Long clanId, Long userId) {
// static으로 Q 클래스 import하거나 변수 선언하기.
// QUser user = QUser.user;
// QClan clan = QClan.clan;
// QUserClan userClan = QUserClan.userClan;
return queryFactory.selectFrom(userClan)
.where(user.id.eq(userId).and(clan.id.eq(clanId)))
.fetchOne();
}
Q 클래스는 satic import 하거나 변수로 선언하여 사용하는데 개인적으로 import 하는게 편한 것 같다.
쿼리 작성은 queryFactory를 빌더패턴으로 생성하면 이해가 빠르다. SELECT, WHERE 같은 구문들을 하나씩 연이어 붙여주고
마지막에 fetch를 해주면 된다.
주요 구문들은 다음과 같다.
- select: entity를 선택하거나 필요한 특정 필드만 선택할 수 있다. 특정 필드만 선택하는 경우 Tuple 객체로 반환.
- from: 타겟 엔티티 테이블 지정. Join의 시작지점이라고 생각.
- selectFrom: 지정된 Q 클래스 엔터티에서 데이터를 선택.
- where: 필터링 조건을 지정합니다. 조건은 BooleanExpression을 사용하여 작성.
- fetch: 쿼리를 실행하고 결과를 리스트로 반환.
- join: 두 엔터티 간의 관계를 조인.
- groupBy: 지정된 필드로 그룹화.
- 집계 함수: select의 필드에 .avg(), .sum(), .count() 등을 덧붙여 데이터 집계를 수행.
위 구문들을 활용하여 아래와 같이 QueryDSL 쿼리를 생성할 수 있다.
List<Tuple> results = jpaQueryFactory.select(user.name, order.count()) // 필요한 필드만 선택
.from(order) // 기본 엔터티 지정
.join(order.user, user) // 조인 관계 설정
.where(user.age.gt(25)) // 조건 추가
.groupBy(user.id) // user로 그룹핑
.fetch();
여기서 중요한 점은 join은 항상 선행되는 parameter가 이미 지정된 entity이어야 한다.
무슨 의미냐면 우리가 from(order)를 했기 때문에 현재 기준 테이블은 order가 되는 것이고, 이 테이블에 user 테이블을 join하는 것이다. 만약 join(user, order.user)라고 순서를 바꾸게 될 경우 user table은 현재 우리가 바라보고 있지 않기 때문에 컴파일 단계에서는 에러가 발생하지 않지만 실행 시 exception이 발생한다.