본문 바로가기
JPA

QueryDsl를 사용해보자.(With Spring boot 3.x.x, JPA)

by khds 2023. 11. 29.

 

들어가기

 

이 글은 Spring boot(3.0 이상), JPA에 QueryDSL을 사용하면서 새로 적용해 봤던 내용들을 정리한 것이다. 

목차는 아래와 같다.

1. QueryDSL은 무엇이며 왜 사용하는가?

2. Spring boot에서 QueryDSL을 사용하기 위한 설정(Java 17, Spring boot 3.x.x를 기준)

3. Spring Data JPA와 같이 사용하는 법

4. DTO 객체로 값을 받는 법

 

 

본론

1. QueryDSL은 무엇이며 왜 사용하는가?

 

JPA를 사용할 때, 편의성을 위해 Spring Data JPA를 종종 사용하였다. 하지만 복잡한 쿼리를 작성해야 할 때면 @Query 어노테이션을 통해 JPQL을 직접 작성하곤 하였다. 로직이 복잡할수록 쿼리 문자열은 길어지며 가독성은 떨어진다. 문자열 중 잘못된 부분이 있을 경우, 런타임 시점에 에러가 발생하기 때문에 사전에 확인하기가 어렵다. 

이런 문제들을 해결해주는 것이 QueryDSL이란 프레임워크이다.

QueryDSL은 정적 타입을 이용하여 SQL과 같은 쿼리를 생성할 수 있게 해 준다.

 

아래의 예시를 봐보자.

public List<Article> findByTitleUnder3() {
    BooleanExpression expression = article.title.length().loe(3);
    return jpaQueryFactory.selectFrom(article).where(expression).fetch();
}

 

위의 코드는 단순히 메서드를 호출하는 것뿐이다. 하지만 이는 아래의 SQL문과 일치한다.

SELECT * FROM ARTICLE WHERE LENGTH(TITLE) <= 3;

 

이렇듯 문자열 형태의 SQL 쿼리를 문자열이 아닌 코드형태로 작성하기 때문에 컴파일 시점에 문법 오류를 쉽게 확인할 수 있다. 

또한 IDE의 도움을 받을 수 있으며 'WHERE'와 같은 제약 조건 등을 메서드 추출을 통해 재사용할 수 있다.

또한 런타임 시에 주어지는 값에 따라 쿼리가 달라지는 동적 쿼리 작성에도 유용하다. 

 

이제 이러한 QueryDSL을 Spring boot, JPA와 함께 사용해 볼 것이다.

참고로 QueryDSL을 통해 엔티티 값을 조회할 때, JPQL을 사용하는 것이므로 영속성 컨텍스트에 엔티티를 보관한다.

그렇기에 'Fetch Join'으로 연관된 테이블의 데이터를 함께 가져오는 것 또한 가능하다.

 

 

2. Spring boot에서 QueryDSL을 사용하기 위한 설정

 

jave version : 17

spring boot : 3.2.3

 

build.gradle

dependencies {
...
//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"
...
}

// QueryDSL
def querydslDir = 'src/main/generated'

//Q 클래스 생성 위치
sourceSets {
    main.java.srcDirs += [ querydslDir ]
}

tasks.withType(JavaCompile) {
    options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
}

clean.doLast {
    file(querydslDir).deleteDir()
}

 

QueryDSL로 쿼리를 작성할 때, 엔티티를 참고하여 생성된 Q 클래스를 사용하여 Type-Safe 한 방식으로 작성한다. sourceSets를 통해 프로젝트 내 Q 클래스 생성 위치를 지정할 수 있다.

 

아래와 같이 JPA를 사용할 때처럼 엔티티를 작성하면, 컴파일 시 Q클래스가 자동으로 생성, 수정된다.

@Entity
@Table(name = "article_table")
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class Article {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "title")
    private String title;

    @Column(name = "content")
    private String content;

    @Column(name = "create_date")
    private Date newDate;

    public Article(String title, String content, Date newDate) {
        this.title = title;
        this.content = content;
        this.newDate = newDate;
    }
}

 

이후 Config 파일을 설정해 줌으로써 프로젝트 전역에서 JPAQueryFactory를 통해 QueryDsl을 사용할 수 있다.

@Configuration
public class QuerydslConfig {
    @PersistenceContext
    private EntityManager entityManager;

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

 

그다음 @Repository를 통해 Bean으로 등록 후, 작성한 메서드를 사용할 수 있다. 

...
import static khds.ecommerce.QArticle.article;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
...

@Repository
public class ArticleRepository {

    private final JPAQueryFactory jpaQueryFactory;

    public ArticleRepository(JPAQueryFactory jpaQueryFactory) {
        this.jpaQueryFactory = jpaQueryFactory;
    }

    @Override
    public List<Article> findByTitleUnder3() {
        BooleanExpression expression = article.title.length().loe(3);
        return jpaQueryFactory.selectFrom(article).where(expression).fetch();
    }

 

간단한 사용예시

//Controller 
@Autowired
private ArticleRepository articleRepository;

@GetMapping("/find")
public List<Article> find(){
    List<Article> articles = articleRepository.findByTitleUnder3();
    return articles;
}

 

 

3. Spring Data JPA와 같이 사용하는 법

 

만약 Spring Data JPA와 동시에 사용 가능한 Repository를 만들고 싶다면?

3개의 파일 작성이 필요하다. 우선 QueryDSL로 작성할 메서드를 담은 인터페이스를 정의한다.

이를 상속받는 구현체에 주요 로직을 작성할 것이다.

public interface ArticleCustomRepository {

    List<Article> findByTitleUnder10();
}

 

그리고 일반적인 Spring Data JPA를 사용하기 위한 interface 정의를 정의한 후, QueryDSL을 사용하는 인터페이스를 상속받는다. 

public interface ArticleRepository extends JpaRepository<Article, Long>, ArticleCustomRepository {
}

 

아래는 QueryDSL을 작성한 클래스이며  ArticleCustomRepository를 구현하고 있다. 

...
import static khds.ecommerce.QArticle.article;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
...

@Repository
public class ArticleCustomRepositoryImpl implements ArticleCustomRepository {

    private final JPAQueryFactory jpaQueryFactory;

    public ArticleCustomRepositoryImpl(JPAQueryFactory jpaQueryFactory) {
        this.jpaQueryFactory = jpaQueryFactory;
    }

    @Override
    public List<Article> findByTitleUnder3() {
        BooleanExpression expression = article.title.length().loe(3);
        return jpaQueryFactory.selectFrom(article).where(expression).fetch();
    }
}

 

작성한 메서드를 사용하는 간단한 API를 호출해 보자.

@RestController
@RequestMapping("/api")
public class TestController {

    @Autowired
    private ArticleRepository articleRepository;
    
    //Spring Data JPA 메서드 사용
    @GetMapping("/find1")
    public List<Article> test1(){
        List<Article> articles = articleRepository.findAll();
        return articles;
    }

	//작성한 QueryDSL 메서드 사용
    @GetMapping("/find2")
    public List<Article> test2(){
        List<Article> articles = articleRepository.findByTitleUnder3();
        return articles;
    }
}

 

findAll()

24.05.23부 테스트 사진 추가

 

findByTitleUnder3()

24.05.23부 테스트 사진 추가

 

 

4. DTO 객체로 값을 받는 법

 

복잡한 연산을 하게 되면, 엔티티가 아닌 원하는 값들로 이루어진 DTO 객체로 받고 싶을 수 있다.

그럴 경우엔 아래와 같이 표현할 수 있다.

...
import static khds.ecommerce.QArticle.article;
...

//ArticleJPAResponse는 DTO
@Override
public List<ArticleJPAResponse> findDtoByTitleUnder3() {
    BooleanExpression expression = article.title.length().loe(3);
    return jpaQueryFactory.select(
            Projections.constructor(ArticleJPAResponse.class, article.id, article.title,
                article.content))
        .from(article).where(expression).fetch();
} // constructor는 생성자, field를 쓸 경우 필드에 직접 주입

 

혹은 DTO 객체에 아래와 같은 어노테이션을 생성자에 추가해 주면 DTO도 Q 객체로 생성된다. 

@Getter
@NoArgsConstructor
public class ArticleJPAResponse {

    private Long id;
    private String title;
    private String content;

    @QueryProjection
    public ArticleJPAResponse(Long id, String title, String content) {
        this.id = id;
        this.title = title;
        this.content = content;
    }
}

 

 

아래와 같이 DTO 생성자를 통해 사용 가능하다. 

...
import static khds.ecommerce.QArticle.article;
import khds.ecommerce.QArticleJPAResponse;
...

@Override
public List<ArticleJPAResponse> findDtoByTitleUnder3() {
    BooleanExpression expression = article.title.length().loe(3);
    return jpaQueryFactory.select(
            new QArticleJPAResponse(article.id, article.title, article.content))
        .from(article).where(expression).fetch();
}

 

간단한 테스트 코드를 실행해 보자.

@GetMapping("/find3")
public List<ArticleJPAResponse> find(){
    List<ArticleJPAResponse> articles = articleRepository.findDtoByTitleUnder3();
    return articles;
}

 

24.05.23부 테스트 사진 추가

 

주의할 점은 DTO로 조회하게 되면 엔티티 조회와 다르게 영속성 컨텍스트에 등록이 안 되는 점을 기억하길 바란다.

 

 

참고 

 

https://tecoble.techcourse.co.kr/post/2021-08-08-basic-querydsl/

 

Spring Boot에 QueryDSL을 사용해보자

1. QueryDSL PostRepository.java Spring Data JPA가 기본적으로 제공해주는 CRUD 메서드 및 쿼리 메서드 기능을 사용하더라도, 원하는 조건의 데이터를 수집하기 위해서는 필연적으로 JPQL…

tecoble.techcourse.co.kr

 

https://velog.io/@soyeon207/QueryDSL-Spring-Boot-%EC%97%90%EC%84%9C-QueryDSL-JPA-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0

 

Spring Boot 에서 QueryDSL JPA 사용하기

QueryDSL 을 본격적으로 사용해보자

velog.io

 

https://velog.io/@juhyeon1114/Spring-QueryDsl-gradle-%EC%84%A4%EC%A0%95-Spring-boot-3.0-%EC%9D%B4%EC%83%81

 

[Spring] QueryDsl gradle 설정 (Spring boot 3.0 이상)

스프링 부트 3.0이상에서의 Querydsl 설정방법

velog.io

 

https://doing7.tistory.com/129

 

[Querydsl] 튜플이나 DTO로 결과 반환하기

프로젝션 : select 대상지정하는 일 프로젝션 대상이 두개 이상이라면 튜플이나 DTO로 조회해야한다. 🌱 튜플 사용하기 com.querydsl.core.Tuple를 사용하고 있다. 때문에 Repository 계층을 넘어서 Service나

doing7.tistory.com

 

https://velog.io/@jinyeong-afk/%EA%B8%B0%EC%88%A0-%EB%A9%B4%EC%A0%91-QueryDSL%EC%9D%B4%EB%9E%80

 

[기술 면접] QueryDSL이란?

QueryDSL은 하이버네이트 쿼리 언어(HQL: Hibernate Query Language)의 쿼리를 타입에 안전하게 생성 및 관리해주는 프레임워크다.QueryDSL은 정적 타입을 이용하여 SQL과 같은 쿼리를 생성할 수 있게 해준다.

velog.io

 

 

https://velog.io/@guns95/QueryDsl-Where-%EB%8B%A4%EC%A4%91-%EC%A1%B0%EA%B1%B4

 

QueryDsl - Where 다중 조건

QueryDsl의 2번째 동적 쿼리 방식인 where의 다중조건 방법을 소개하겠다. 우선 builder 패턴 보다 이 방법을 추천한다. 그이유는 몇가지가 있는데 첫번째는 동적쿼리를 작성한 메서드를 볼 때 가독성

velog.io