JPA 관련 내용 중에서 QueryDSL에 대해서 알아보았다.
JpaRepository는 기본적인 CRUD와 findBy, existBy, countBy 등
다양한 데이터베이스 관련 메소드를 구현해주고 있지만
특정 상황에선 직접 Query를 입력할 필요가 있다.
여기서 QueryDSL를 접하게 되었고
어떤 상황에서 쓰이는지, 어떤 특징을 가지는지 작성하려 한다.
✅ QueryDSL이란?
QueryDSL은 타입 안전(Type-Safe)한 쿼리를 생성할 수 있도록 도와주는
Java 기반의 쿼리 프레임워크이다.
주로 JPA, JDBC, MongoDB 등 다양한 데이터 소스와 함께 사용할 수 있으며,
SQL이나 JPQL을 직접 작성하는 것보다 가독성이 높고
유지보수가 쉬운 코드를 작성할 수 있다.
QueryDSL의 특징
1. 타입 안전성 (Type-Safety)
- JPQL이나 네이티브 SQL은 문자열 기반으로 컴파일 단계에서 문법 오류를 잡을 수 없지만
QueryDSL은 메서드 기반으로 컴파일 타임에 오류를 잡을 수 있다.
2. 가독성과 유지보수성 증가
- 문자열 기반 쿼리는 가독성이 떨어지고 유지보수가 어렵지만
QueryDSL은 메서드 체이닝 방식으로 가독성이 좋고, IDE의 자동완성 기능도 활용할 수 있다.
3. 동적 쿼리(Dynamic Query) 지원
- BooleanBuilder, Expressions 등을 활용하면 조건에 따라 동적으로 쿼리를 구성할 수 있다.
4. JPQL 및 SQL과의 호환성
- 기존의 JPQL과 SQL을 QueryDSL을 사용하여 더욱 직관적이고 안정적인 코드로 변환할 수 있다.
QueryDSL 사용법
1. QueryDSL 의존성 추가
dependencies {
implementation 'com.querydsl:querydsl-jpa:5.0.0'
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jpa'
}
2. QueryDSL 코드 예제
- JPAQueryFactory를 사용하여 QueryDSL을 실행
- QMember 클래스를 사용하여 쿼리를 안전하게 생성 (QMember는 QueryDSL이 자동 생성)
- where()을 활용하여 나이 조건에 맞는 데이터를 필터링
1) Entity 클래스 생성
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
private int age;
}
2) QueryDSL을 활용한 Repository 코드
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import java.util.List;
import static com.example.demo.entity.QMember.member; // QueryDSL이 자동 생성한 Q클래스
@Repository
public class MemberQueryRepository {
private final JPAQueryFactory queryFactory;
public MemberQueryRepository(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}
public List<Member> findMembersByAge(int age) {
return queryFactory
.selectFrom(member)
.where(member.age.eq(age)) // 나이가 특정 값과 같은 회원 조회
.fetch();
}
}
QueryDSL을 활용한 동적 쿼리
- if 문을 사용해 원하는 조건만 추가 가능
- BooleanBuilder를 사용해 가독성이 좋아짐
public List<Member> searchMembers(String name, Integer age) {
BooleanBuilder builder = new BooleanBuilder();
if (name != null) {
builder.and(member.name.eq(name));
}
if (age != null) {
builder.and(member.age.eq(age));
}
return queryFactory
.selectFrom(member)
.where(builder)
.fetch();
}
QueryDSL 정리
✅ 장점
✔️ 타입 안전성 보장
✔️ 가독성이 좋고 유지보수가 쉬움
✔️ 동적 쿼리를 쉽게 작성 가능
❌ 단점
⚠️ 설정이 다소 복잡할 수 있음
⚠️ 프로젝트 빌드 시 QClass 파일을 자동 생성해야 함
QueryDSL을 사용하는 상황
1. 복잡한 동적 쿼리가 필요한 경우
JpaRepository만으로는 이런 경우를 처리하기 어렵고, @Query로 JPQL을 직접 작성하면 유지보수가 어려움
이때, QueryDSL을 활용하면 깔끔하고 유지보수하기 쉬운 코드 작성 가능
/*
name이 특정 값과 같거나
age가 특정 범위에 있거나
city가 특정 도시명과 일치하는 회원을 조회
*/
public List<Member> searchMembers(String name, Integer age, String city) {
BooleanBuilder builder = new BooleanBuilder();
if (name != null) {
builder.and(member.name.eq(name));
}
if (age != null) {
builder.and(member.age.gt(age));
}
if (city != null) {
builder.and(member.city.eq(city));
}
return queryFactory
.selectFrom(member)
.where(builder)
.fetch();
}
2. 복잡한 다중 테이블 조인이 필요한 경우
JpaRepository에서는 간단한 관계 조회는 가능하지만
여러 개의 테이블을 조인하는 쿼리는 QueryDSL이 편리함
// 회원과 팀을 조인하여 특정 팀의 회원 목록을 가져오기
// JPQL 사용
@Query("SELECT m FROM Member m JOIN m.team t WHERE t.name = :teamName")
List<Member> findMembersByTeamName(@Param("teamName") String teamName);
// QueryDSL 사용
public List<Member> findMembersByTeamName(String teamName) {
return queryFactory
.selectFrom(member)
.join(member.team, team)
.where(team.name.eq(teamName))
.fetch();
}
3. 페이징 및 정렬이 필요한 경우
Spring Data JPA의 Pageable을 이용한 기본적인 페이징 기능은 사용할 수 있지만
복잡한 페이징 쿼리가 필요한 경우 QueryDSL이 더 효과적
// 회원 목록을 정렬하여 페이징 처리
public Page<Member> findMembersWithPagination(Pageable pageable) {
List<Member> results = queryFactory
.selectFrom(member)
.orderBy(member.age.desc()) // 나이 내림차순 정렬
.offset(pageable.getOffset()) // 페이지 오프셋 설정
.limit(pageable.getPageSize()) // 페이지 크기 설정
.fetch();
long total = queryFactory
.selectFrom(member)
.fetchCount(); // 전체 데이터 개수 조회
return new PageImpl<>(results, pageable, total);
}
4. 성능 최적화가 필요한 경우 (서브쿼리, 벌크 연산 등)
Spring Data JPA에서 지원하지 않는 서브쿼리, 벌크 업데이트 등을 사용해야 할 때
// 나이가 평균보다 많은 회원 조회 (서브쿼리 사용)
QMember subMember = new QMember("subMember");
public List<Member> findMembersAboveAverageAge() {
return queryFactory
.selectFrom(member)
.where(member.age.gt(
JPAExpressions
.select(subMember.age.avg()) // 서브쿼리 사용
.from(subMember)
))
.fetch();
}
5. 벌크 연산 (대량 업데이트/삭제)이 필요한 경우
JPA의 save() 메서드는 엔티티를 한 개씩 업데이트하는 방식으로 대량 데이터 업데이트가 비효율적
QueryDSL을 사용하면 한 번의 SQL로 여러 데이터를 일괄 업데이트 가능
// 특정 조건에 해당하는 회원의 나이를 일괄 업데이트
public long updateMemberAge(int oldAge, int newAge) {
return queryFactory
.update(member)
.set(member.age, newAge)
.where(member.age.eq(oldAge))
.execute();
}
이렇게 다양한 상황에서 복잡한 쿼리를 사용하기 위해 QueryDSL을 사용한다.
특히 문자열 검색에서 LIKE를 사용하게 된다면
JpaRepository에선 findBy(Field)Containing(String keyword)를 사용하여
%(String)% 형태만 문자열 조회가 가능하지만
QueryDSL을 사용하면 체이닝 메소드 방식으로 유연하게 LIKE 검색이 가능하다.
public List<Member> searchMembersByName(String keyword) {
return queryFactory
.selectFrom(member)
.where(member.name.like("%" + keyword + "%")) // LIKE '%keyword%'
.fetch();
}
여기서 추가로 QueryDSL의 조회 관련 메소드를 정리해봤다.
비교 연산자
| .eq(value) | 값이 일치하는지 비교 (Equal) | WHERE column = value |
| .ne(value) | 값이 일치하지 않는지 비교 (Not Equal) | WHERE column <> value |
| .gt(value) | 초과 (Greater Than) | WHERE column > value |
| .goe(value) | 이상 (Greater or Equal) | WHERE column >= value |
| .lt(value) | 미만 (Less Than) | WHERE column < value |
| .loe(value) | 이하 (Less or Equal) | WHERE column <= value |
문자열 검색
| .like("%값%") | 특정 문자열을 포함하는지 검사 | WHERE column LIKE '%값%' |
| .contains(value) | 특정 문자열 포함 (LIKE '%값%') | WHERE column LIKE '%값%' |
| .startsWith(value) | 특정 문자열로 시작 (LIKE '값%') | WHERE column LIKE '값%' |
| .endsWith(value) | 특정 문자열로 끝남 (LIKE '%값') | WHERE column LIKE '%값' |
논리 연산
| .and(condition) | AND 조건 | WHERE col1 = value1 AND col2 = value2 |
| .or(condition) | OR 조건 | WHERE col1 = value1 OR col2 = value2 |
NULL 체크
| .isNull() | 값이 NULL인지 체크 | WHERE column IS NULL |
| .isNotNull() | 값이 NULL이 아닌지 체크 | WHERE column IS NOT NULL |
IN 절
| .in(value1, value2, ...) | 특정 값 목록에 포함되는지 검사 | WHERE column IN (value1, value2, ...) |
| .notIn(value1, value2, ...) | 특정 값 목록에 포함되지 않는지 검사 | WHERE column NOT IN (value1, value2, ...) |
BETWEEN
| .between(value1, value2) | 값이 특정 범위에 포함되는지 검사 | WHERE column BETWEEN value1 AND value2 |
정렬
| .orderBy(column.asc()) | 오름차순 정렬 | ORDER BY column ASC |
| .orderBy(column.desc()) | 내림차순 정렬 | ORDER BY column DESC |
페이징
| .offset(n) | 결과를 n개 건너뛰기 | OFFSET n |
| .limit(n) | 최대 n개 가져오기 | LIMIT n |
이렇게 알아보니 직접 Query를 작성하는 것보다 가독성도 좋고
JpaRepository의 한계도 해결할 수 있는 QueryDSL을
실제 프로젝트에서는 어떻게 활용할 수 있을지 고민해볼 수 있는 계기가 됐다.
'Java > SpringBoot' 카테고리의 다른 글
| 영속성(Persistence)이란 무엇일까? (1) | 2025.02.03 |
|---|---|
| Security Filter Chain (0) | 2025.01.31 |
| 자바의 Filter에 대해서 (0) | 2025.01.31 |
| 인증과 인가 (0) | 2025.01.30 |
| JWT 로그인 테스트-4 (0) | 2025.01.22 |