본문 바로가기
Java/SpringBoot

QueryDSL에 대한 공부

by wooksss 2025. 2. 10.

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