JPA

[JPA] QueryDSL 이란?

tudamoa 2025. 6. 28. 00:26

✅ QueryDSL이란 무엇인가?

DSL는 Domain Specific Language의 약자이다. 말 그대로 특정 영역의 언어이다.
앞에 Query라는 단어가 붙었으니 쿼리 영역에 집중하는 언어라고 생각할 수 있다.

QueryDSL은 Java 기반의 타입 안전한 SQL/JPQL 쿼리 생성 라이브러리다.

 

보통 JPA를 쓸 때 JPQL을 문자열로 작성한다. 예를 들어,

String jpql = "select a from Article a where a.title like :title";

 

하지만 이렇게 문자열로 쿼리를 쓰면

  • 오타가 있어도 컴파일러가 못 잡아준다.
  • 복잡한 동적 쿼리를 만들기 어렵다.
  • 리팩토링이 힘들다.

여기서 QueryDSL은 이 문제를 해결해 준다.

  • 컴파일 시점에 쿼리 오류를 잡을 수 있다.
  • 동적 쿼리를 간결하게 작성할 수 있다.
  • IDE 자동완성이 된다.

예를 들어 QueryDSL로 같은 쿼리를 작성하면 아래처럼 된다:

QArticle article = QArticle.article;

List<Article> articles = queryFactory
        .selectFrom(article)
        .where(article.title.contains("QueryDSL"))
        .fetch();

 

위 코드를 보면 모든 속성과 테이블이 타입으로 관리되며
오타나 잘못된 조인 등을 컴파일 단계에서 체크할 수 있다.

 

복잡한 조건, 동적 정렬, 서브쿼리까지
마치 자바 코드를 짜듯이 쿼리를 만들 수 있다.

 

정리하자면 QueryDSL은

→ JPQL도 SQL도 더 안전하고 편리하게 쓸 수 있게 해주는 도구이며 JPA와 상호보완적인 관계이다.

 


✅ QueryDSL의 장점

  1. 타입 안전성 (Type-Safe)
    • JPQL을 문자열로 작성하면 오타나 잘못된 필드명을 컴파일러가 잡아주지 못한다.
    • QueryDSL은 Q타입 클래스를 이용해 쿼리를 작성하므로
      컴파일 단계에서 오류를 검출할 수 있다.
    • ex) article.title.eq("Hello")
      → title이 존재하지 않으면 컴파일 에러 
  2. IDE 자동완성 지원
    • Q타입 클래스 덕분에 자동완성 기능이 매우 강력하다.
    • 덕분에 쿼리 작성이 훨씬 빠르고 실수가 줄어든다.
  3. 동적 쿼리 작성이 쉽다
    • BooleanBuilder 나 where 절에 여러 조건을 체이닝하며
      가변적으로 쿼리를 조립할 수 있다.
    • ex)
      BooleanBuilder builder = new BooleanBuilder();
      if (title != null) {
          builder.and(article.title.contains(title));
      }
  4. 복잡한 쿼리 작성에 유리
    • 서브쿼리, 조인, 그룹핑 등 복잡한 SQL도
      마치 자바 코드처럼 깔끔하게 작성할 수 있다.
  5. JPQL, SQL 모두 지원
    • JPA 기반 JPQL뿐 아니라 Native SQL도 작성 가능하다.
    • 복잡한 Native Query가 필요할 때도 QueryDSL로 깔끔하게 처리 가능.

❌ QueryDSL의 단점

  1. 빌드 과정이 복잡해질 수 있다
    • Q타입 파일 생성 과정(annotation processor)을
      빌드 툴(Gradle, Maven)에 설정해야 한다.
    • 처음 세팅할 때 약간 복잡하다.
  2. 학습 비용
    • JPQL만 쓰던 개발자라면 처음에 문법을 익히는 데 시간이 필요하다.
  3. 의존성 추가가 필요
    • QueryDSL 라이브러리와 APT(annotation processor)를 추가해야 한다.

 


Q타입 클래스란?

Q타입클래스가 없으면 QueryDSL은 사용이 불가능하다.

Q타입클래스는 QueryDSL이 컴파일 할때 자동으로 생성해주는 쿼리 전용 클래스이다.

JPA 엔티티를 자바 코드에서 타입 안전하게 다룰 수 있게 해주는 일종의 쿼리 전용 DTO 같은 존재다.

예) Article 엔티티:

@Entity
public class Article {
    @Id
    private Long id;
    private String title;
}

 

빌드할 때 만들어지는 QArticle (Q타입 클래스):

public class QArticle extends EntityPathBase<Article> {

    public static final QArticle article = new QArticle("article");

    public final StringPath title = createString("title");
    public final NumberPath<Long> id = createNumber("id", Long.class);

    ...
}

 

이렇게 Q타입클래스를 만들어 놓으면,

QArticle article = QArticle.article;

List<Article> list = queryFactory
    .selectFrom(article)
    .where(article.title.eq("Hello"))
    .fetch();

 

이렇게 코드를 작성했을 때

 

  • article.title → title 컬럼
  • article.id → id 컬럼

으로 매칭 시켜주어서 모든 필드를 타입 세이프하게 접근 가능하다.

 

또한 Q타입클래스는 따로 설정해서 생성 빌드 경로도 정해주어야 한다.

 


 

🚀 QueryDSL 활용

✅ 동적쿼리

예) 조건이 있을 때만 where 절을 추가하고 싶을 때 ↓

BooleanBuilder builder = new BooleanBuilder();

if (title != null) {
    builder.and(article.title.contains(title));
}

if (authorName != null) {
    builder.and(article.author.name.contains(authorName));
}

List<Article> list = queryFactory
    .selectFrom(article)
    .where(builder)
    .fetch();
  • title과 authorName이 null이 아니라면 where 조건을 추가하는 코드이다.
  • 만약 조건이 입력된다면,
String title = "hello";
builder.and(article.title.contains(title));

이 Java 코드는,

WHERE title LIKE '%hello%'

이 SQL문으로 변역이 되는 식이다.

 

✅ Fetch Join

예) Article → Member(author) join을 fetch join 하려면 ↓

QArticle article = QArticle.article;
QMember member = QMember.member;

List<Article> articles = queryFactory
    .selectFrom(article)
    .leftJoin(article.author, member).fetchJoin()
    .fetch();

 

  • fetchJoin() 메서드만 붙여주면 된다.
  • 연관 엔티티를 한 번에 조회해서 N+1 문제를 방지한다.

 

✅ 서브쿼리

예) 조회수(hit)가 전체 평균 이상인 게시글 찾기 ↓

QArticle article = QArticle.article;
QArticle subArticle = new QArticle("subArticle");

List<Article> list = queryFactory
    .selectFrom(article)
    .where(
        article.hit.goe( // goe = greater or equal
            JPAExpressions
                .select(subArticle.hit.avg())
                .from(subArticle)
        )
    )
    .fetch();
    
//SQL로 바꾸면 아래처럼
SELECT *
FROM article
WHERE hit >= (SELECT AVG(hit) FROM article)

 

 

JPAExpressions 를 사용해 서브쿼리를 작성 (서브쿼리 생성 도우미 클래스)

  • select, where, having 절에 서브쿼리 가능
  • 타입 안전하게 작성 가능

✅ QueryDSL이 서브쿼리를 쉽게 만들어주는 게 큰 장점이다.

 


 

✅ QueryDSL 설정 

 

💡 build.gradle.kts 설정

plugins {
	java
	id("org.springframework.boot") version "3.4.3"
	id("io.spring.dependency-management") version "1.1.7"
}

group = "study"
version = "0.0.1-SNAPSHOT"

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(21)
	}
}

configurations {
	compileOnly {
		extendsFrom(configurations.annotationProcessor.get())
	}
}

repositories {
	mavenCentral()
}

// QueryDSL 
val querydslVersion = "5.0.0"

dependencies {
	implementation("org.springframework.boot:spring-boot-starter-data-jpa")
	implementation("org.postgresql:postgresql")
	implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
	implementation("org.springframework.boot:spring-boot-starter-web")
	compileOnly("org.projectlombok:lombok")
	annotationProcessor("org.projectlombok:lombok")
	testImplementation("org.springframework.boot:spring-boot-starter-test")
	testRuntimeOnly("org.junit.platform:junit-platform-launcher")
	implementation("com.h2database:h2")
	implementation("org.springframework.boot:spring-boot-starter-validation")

	// QueryDSL
	implementation("com.querydsl:querydsl-jpa:${querydslVersion}:jakarta")
	annotationProcessor("com.querydsl:querydsl-apt:${querydslVersion}:jakarta")
	annotationProcessor("jakarta.annotation:jakarta.annotation-api")
	annotationProcessor("jakarta.persistence:jakarta.persistence-api")
}

tasks.withType<Test> {
	useJUnitPlatform()
}
tasks.withType<JavaCompile> {
	options.encoding = "UTF-8"
	options.compilerArgs.add("-parameters")
}
tasks.withType<JavaExec> {
	systemProperty("file.encoding", "UTF-8")
}

// QueryDSL
val generatedDir = layout.buildDirectory.dir("generated/sources/annotationProcessor/java/main")

// QueryDSL
sourceSets["main"].java {
	srcDir(generatedDir)
}
// QueryDSL
tasks.named<Delete>("clean") {
	delete(generatedDir)
}

 

이렇게 설정하고 Gradle bulid를 하고 

 

 

💡 JPAQueryFactory 빈 등록

@Configuration
@RequiredArgsConstructor
public class QuerydslConfig {

    private final EntityManager em;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(em);
    }
}

 

이대로 실행하면

위 두 개의 Entity가 있다면,

bulid/generated/sources/annotationProcessor/java/main

아까 build.gradle.kts에서 설정해둔 위 경로로

QArticle, QMember 두 클래스가 생성된다.

 

생성이 완료되었다면,

@Repository
@RequiredArgsConstructor
public class ArticleRepositoryImpl implements ArticleRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    @Override
    public Page<Article> findByTitle(String title, Pageable pageable) {
            QArticle article = QArticle.article;

            BooleanExpression titleCondition =
                    (title != null && !title.isBlank()) ? article.title.contains(title) : null;

            List<Article> content = queryFactory
                    .selectFrom(article)
                    .where(titleCondition)
                    .offset(pageable.getOffset())
                    .limit(pageable.getPageSize())
                    .fetch();

            Long total = queryFactory
                    .select(article.count())
                    .from(article)
                    .where(titleCondition)
                    .fetchOne();

            return new PageImpl<>(content, pageable, total != null ? total : 0L);
        }
     }

 

이런 식으로 DSL을 활용할 수 있다.

 

 

'JPA' 카테고리의 다른 글

[JPA] 다중성이란?  (0) 2025.06.26
[JPA] Fetch Join이란?  (0) 2025.06.25
[JPA] N+1 문제 원인 & 해결  (0) 2025.06.24