JPARepository 에서 제공하는 메서드로 해결이 안되는 경우 동적 쿼리를 짜야한다. 간단하게는 native query 로 해결할 수 있는데 개발자가 실수를 한 경우에 이는 컴파일 시점이 아닌 런타임 시점에서 고객이 그 메서드를 호출하는 행위를 한 경우에만 오류를 확인할 수 있게 된다. 이를 해결하기 위해 QueryDSL 을 적용해보자.
QueryDSL 설정
1. spring boot 3.x QueryDSL 의존성 주입 ( Gradle 기준 )
// 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'
2. build or compileJava
- build : 컴파일, 테스트 실행, 패키징, 필요한 문서 생성 등 프로젝트를 완전히 빌드하고 준비하는데 필요한 모든 하위 태스크를 포함한다. 즉, 소스 코드가 컴파일되고 테스트가 수행되며 JAR 파일이 생성된다.
- compileJava : 자바 소스 파일을 컴파일한다. 소스에서 Java 소스 파일만을 대상으로 하며, 이러한 소스 파일을 JVM 에서 실행할 수 있는 바이트코드로 변환한다. build 의 일부로 자동 실행되며 자바 소스 코드만 컴파일하고 싶을 때 따로 실행할 수 있다.

3. 실행결과
- 위 작업으로 수행된 결과물로 Q클래스를 확인할 수 있다.

사용자 정의 Repository 구현
JPARepository 를 상속받는 것 외에 쿼리를 커스텀하기 위해 사용자 정의 Repository 를 구현해보자.
1. 사용자 정의 Repository 구현
Repository 를 커스텀하여 사용하기 위해 먼저 CustomRepository 인터페이스와 구현체를 생성한다.
- Custom Repository 인터페이스
public interface UserCustomRepository {
List<User> findUserByQueryDSL(String email, String name, Integer age);
}
- Custom Repository 구현체
Custom Repository 구현체의 기본 접미사는 Impl 이다.
설정 변경이 가능하며, 이는 Spring Data 가 구현체를 찾는 데 사용된다.
public class UserCustomRepositoryImpl implements UserCustomRepository {
private final JPAQueryFactory queryFactory; // JPA 쿼리를 구성하고 실행
public UserCustomRepositoryImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em); // 데이터베이스 연결 및 트랜잭션 관리
}
@Override
public List<User> findUserByQueryDSL(String email, String name, Integer age) {
return null;
}
}
그리고 기존 Repository 에 Custom Repository 를 추가하여 확장한다.
- UserRepository 인터페이스
public interface UserRepository extends JpaRepository<User,Long>,UserCustomRepository {
}
- Diagram

2. 사용자 정의 Repository 구현 시 단점
- 사용자 정의 인터페이스와 구현체를 추가함으로써 프로젝트 구조가 복잡해진다.
- 사용자 정의 구현체가 많아질수록 관계를 이해하고 유지보수하는 것이 어려워질 수 있다.
QueryDSL 기본 문법 및 동적 쿼리
1. QueryDSL 일반적인 사용
- from : 쿼리 소스를 정의한다.
List<User> users = queryFactory
.selectFrom(user)
.fetch();
- innerJoin, join, leftJoin, fullJoin, with : 조인 요소를 정의한다. 첫 번째 인수는 조인 소스이고 두 번째 인수는 대상이다.
List<User> usersWithDepartment = queryFactory
.selectFrom(user)
.innerJoin(user.department, QDepartment.department)
.fetch();
- where : 쉼표 혹은 and 연산자를 통해 쿼리 필터를 정의한다.
List<User> usersByName = queryFactory
.selectFrom(user)
.where(user.name.eq("John Doe"))
.fetch();
- groupBy : varargs 형태로 그룹별 인자를 정의한다.
- having : group by 의 필터 조건을 varags 배열로 정의한다.
List<Tuple> usersByAge = queryFactory
.select(user.age, user.count())
.from(user)
.groupBy(user.age)
.having(user.age.gt(18))
.fetch();
- orderBy : 결과의 순서를 varargs 배열로 추가한다. asc() 및 desc() 사용 가능
List<User> usersOrderedByAge = queryFactory
.selectFrom(user)
.orderBy(user.age.desc())
.fetch();
- limit, offset, restrict : 결과의 페이징을 설정한다. limit 으로 최대 결과 수를 제한하고 offset 으로 행을 건너뛸 수 있으며 둘을 한 번에 정의하기 위해 restrict 를 사용한다.
List<User> paginatedUsers = queryFactory
.selectFrom(user)
.orderBy(user.name.asc())
.limit(10) // 페이지당 결과 수
.offset(20) // 20개 결과를 건너뜀 (-> 세 번째 페이지)
.fetch();
2. BooleanExpression 으로 동적 쿼리 짜기
사용자의 요구사항에 따라 쿼리의 조건이 변경되어야 할 때 QueryDSL 을 사용하면 코드를 간결하게 만들 수 있다.
위에서 Custom Repository에 정의한 findUserByQueryDsl( .. ) 을 구현해보자.
- 사용자 요구사항 3가지
1. 이름으로 조회
2. 특정 패턴이 들어가있는 이메일 사용자 조회 ( ex> naver.com 등 )
3. 특정 나이 이상의 사용자 조회
- UserCustomRepository 구현체 내부 로직 작성하기
BooleanExpression 은 조건을 나타내는 객체로 복잡한 쿼리 조건을 쉽게 구성할 수 있게 한다.
또한, where 절에서 조건이 null 값인 경우 해당 조건은 무시된다.
public class UserCustomRepositoryImpl implements UserCustomRepository {
private final JPAQueryFactory queryFactory;
public UserCustomRepositoryImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}
@Override
public List<User> findUserByQueryDSL(String emailPattern, String name, Integer age) {
return queryFactory.selectFrom(user)
.where( userNameEq(name), // 요구사항 1
emailPatternContain(emailPattern), // 요구사항 2
ageGoe(age)) // 요구사항 3
.fetch();
}
private BooleanExpression userNameEq(String userName) { // 요구사항 1
return StringUtils.isEmpty(userName) ? null : user.userName.eq(userName);
}
private BooleanExpression emailPatternContain(String pattern) { // 요구사항 2
return StringUtils.isEmpty(pattern) ? null : user.email.contains(pattern);
}
private BooleanExpression ageGoe(Integer age) { // 요구사항 3
return age == null ? null : user.age.goe(age);
}
}
정리
사이드 프로젝트 시에 프로젝트 규모가 크지 않고 비즈니스 로직이 단순한 편임에도 QueryDSL 을 억지로 적용하기도 했었는데 프로젝트 구조적인 측면에서 고려를 하지 못했던 것 같다. 그렇지만 컴파일 시점에서 오류를 잡을 수 있고 동적 쿼리를 간결하게 적용할 수 있다는 점에서 매우 유용한 도구라고 생각한다.
참고
http://querydsl.com/static/querydsl/2.8.2/reference/html/ch02.html#d0e166
실전! Querydsl 강의 - 인프런
Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!, 복잡한 쿼리, 동적 쿼리는 이제 안녕! Querydsl로 자바 백엔드 기술을 단단하게. 🚩 본 강의는 로드맵 과정입니다. 본 강의는 자바 백엔
www.inflearn.com
https://docs.spring.io/spring-data/jpa/reference/repositories/custom-implementations.html
'Web' 카테고리의 다른 글
| [JPA] @ColumnDefault 이해하기 (0) | 2024.01.28 |
|---|---|
| [Spring] Soft Delete with JPA (0) | 2024.01.14 |
| [JPA] 양방향 순환 참조 (0) | 2023.12.24 |
| [Spring] 예외 코드 Enum 으로 관리하기 (0) | 2023.12.17 |
| [Spring] actuator 를 통한 shutdown endpoint 생성 (0) | 2023.12.10 |