본문 바로가기
Programming/Spring

[JPA] QueryDSL

 

 

 

앞선 내용에서는 @Query를 사용해서 직접 JPQL의 쿼리를 작성했다. 하지만, 직접 문자열을 입력하기 때문에 컴파일 시점에 에러를 잡지 못한다. 문자열이 잘못된 경우에는 어플리케이션이 실행된 후 로직이 실행되고 나서야 오류를 발견할 수 있기 때문에, 배포 후에 큰 리스크를 떠안게 될 수도 있다. 

 

가독성도 떨어진다.

 

이를 해소하기 위해 등장한 것이 queryDSL이다. 쿼리를 문자열로 작성하는 것이 아니라 코드로 작성할 수가 있다!

 

 

QueryDSL 

정적 타입을 이용해 SQL과 같은 쿼리를 생성할 수 있도록 지원하는 프레임워크다. 문자열이나 XML 파일을 통해 쿼리를 작성하는 대신 플루언트 API를 활용해 쿼리를 생성할 수 있다. 

 

IDE가 제공하는 코드 자동 완성 기능을 사용할 수 있고,

 

문법적으로 잘못된 쿼리를 허용하지 않기 때문에 (정상적인 QueryDSL이라면) 오류를 발생시키지 않으며

 

고정된 SQL 쿼리를 작성하지 않기 때문에 동적으로 쿼리를 생성할 수 있고, 

 

가독성 및 생산성이 향상된다. 

 

 

Gradle에 주입 

 

application.properties

 

 

설정 후 간단한 Entity를 만들고 실행하면 아래와 같이 테이블이 생성된다. 

 

 

👇👇👇👇 gradle 전체 코드👇👇👇👇

더보기
buildscript {
	ext {
		queryDslVersion = "5.0.0"
	}
}

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.3.0'
	id 'io.spring.dependency-management' version '1.1.5'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

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

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'com.mysql:mysql-connector-j'
	annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'


	implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
	annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
	annotationProcessor "jakarta.annotation:jakarta.annotation-api"
	annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

tasks.named('test') {
	useJUnitPlatform()
}
// querydsl 세팅 시작
def querydslDir = "$buildDir/generated/querydsl"

sourceSets {
	main.java.srcDir querydslDir
}
configurations {
	querydsl.extendsFrom compileClasspath
}

 

주의해야 할 점

implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
implementation "com.querydsl:querydsl-apt:${queryDslVersion}"

 

위와 같은 방식은 스프링 2.X 버전에서 사용하는 의존 주입 방식이다. 이와 같이 사용하면 아래와 같은 에러가 발생한다. 

 

이제 실행을 해보면, 우리가 설정했던 

해당 경로로 Q도메인이 생성된다. 

 

QueryDSL은 지금까지 작성했던 엔티티 클래스와 Q도메인이라는 쿼리 타입의 클래스를 자체적으로 생성해서 메타데이터로 사용한다. 이를 통해 SQL과 같은 쿼리를 생성해서 제공한다. 

 

@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QProduct extends EntityPathBase<Product> {

    private static final long serialVersionUID = -1940086066L;

    public static final QProduct product = new QProduct("product");

    public final DateTimePath<java.time.LocalDateTime> createdAt = createDateTime("createdAt", java.time.LocalDateTime.class);

    public final StringPath name = createString("name");

    public final NumberPath<Long> number = createNumber("number", Long.class);

 

이런 모양새다. 

 

Q도메인 클래스는 JPA 엔티티를 기반으로 생성되어 해당 엔티티의 필드와 관계를 정적으로 나타내는 java 클래스다. 따라서 이 클래스들을 사용하면 쿼리를 작성할 때 문자열 기반의 필드명이나 테이블명을 사용하는 것보다 타입 안정성을 제공 받을 수 있다. 

 

queryDSL 장점 중 - (문법적으로 잘못된 쿼리를 허용하지 않기 때문에 (정상적인 QueryDSL이라면) 오류를 발생시키지 않으며)

 

 

간단하게 select 해보기 - JPAQuery

public class ProductRepositoryTest {
    @PersistenceContext
    EntityManager entityManager;

    @Test
    void queryDslTest(){
        JPAQuery<Product> query = new JPAQuery<>(entityManager);
        QProduct qProduct = QProduct.product;

        List<Product> ProductList = query
                .from(qProduct)
                .where(qProduct.name.eq("펜"))
                .orderBy(qProduct.price.asc())
                .fetch();
    }

}

 

queryDSL을 사용하기 위해서는 JPAQuery 객체를 사용한다. JPAQuery는 Entity Manager를 사용해 생성한다. 이렇게 생성된 JAPQuery는 builder 형식으로 쿼리를 작성할 수 있다. (손 쉽게 SQL문 작성 가능!) 

 

예제와 같이 리스트 형식을 return 받으려면 fetch() 메서드를  사용해야 한다. 한 건의 조회 경우 fetchOne, 여러 건의 조회 결과 중 1건 반환은 fetchFirst(), 조회 결과의 개수는 fetchCount(), 조회 결과 리스트와 개수를 포함한 것은 fetchResults로.

 

@PersistenceContext

JPA에서 사용한다. Entity Manager를 필드나 메서드 매개변수로 주입할 때 사용한다. Entity Manager는 JPA에서 영속성 컨텍스트를 관리한다. 트랜잭션 관리, 데이터베이스와 상호작용(CRUD) 등을 지원한다. 


간단하게 select 해보기 - JPAQueryFactory

public class ProductRepositoryTest {
    @PersistenceContext
    EntityManager entityManager;

    @Test
    void queryDslTest(){
        JPAQueryFactory query = new JPAQueryFactory(entityManager);
        QProduct qProduct = QProduct.product;

        List<Product> ProductList = query.selectFrom(qProduct)
                .from(qProduct)
                .where(qProduct.name.eq("펜"))
                .orderBy(qProduct.price.asc())
                .fetch();
    }

}

 

다른 점이 있다면 JPAQueryFactory를 선언할 때 Entity의 타입을 명시해주지 않아도 되고 selectFrom부터 시작한다는 것. 

만약 전체가 아니라 일부만 조회하고 싶다면 .select() .from()과 같이 분리해서 작성하면 된다. 또한, select()에 select(a,b,c)와 같이 조회하고 싶은 일부만 뽑아낼 수 있다. 

 

 

QueyrDSL Config를 만들어서 재생성을 방지해보자 

앞서 QueryDSL 실습을 완료했다. 하지만, 지금은 실행 할 때마다 계속해서 새로운 JPAQueryFactory를 만들고 있다. JPAQueryFactory 객체를 @Bean 객체로 등록해두면 앞에서 작성한 예제처럼 매번 JPAQueryFactory를 초기화하지 않고 스프링 컨테이너에서 가져다 쓸 수 있다. 

 

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QueryDSLConfiguration {


    @PersistenceContext
    EntityManager entityManager;

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

 

 

아까 작성했던 테스트 코드에서 JPAQueryFactory를 생성했던 부분에 이를 대신해서 넣어준다. 

 

@SpringBootTest
public class ProductRepositoryTest_config {

    @Autowired
    ProductRepository productRepository;
    @Autowired
    JPAQueryFactory jpaQueryFactory;

    @Test
    void queryDslTest(){

        QProduct qProduct = QProduct.product;


        Product product = jpaQueryFactory
                .selectFrom(qProduct)
                .where(qProduct.name.eq("pencil"))
                .orderBy(qProduct.price.asc())
                .fetchOne();


    }

}

 

@Configuration

클래스 선언부에 적용되며, 하나 이상의 Bean 메서드를 포함한다. Spring IoC 컨테이너가 빈을 생성하고 관리할 수 있도록 만들어준다. 
@Configurable은 도메인 객체는 Bean 아니지만 도메인 객체에 DAO나 Repository를 주입시켜줘야 하는 경우가 있다. 하지만 DI를 사용하려면 스프링이 관리하는 Bean이어야 하기때문에 도메인 객체들의 생성을 스프링 컨테이너가 관리할 수 없다. 왜냐하면 애플리케이션 동작 중에 생성되기 때문에 스프링이 생명주기를 관리할 bean으로 등록할 수가 없는 것이다. 
이때 @Configurable을 사용하는데, 이것이 붙어있는 클래스를 스프링에 등록해두면 스프링은 객체가 생성될 때 그 객체가 필요로 하는 Bean들을 주입해준다. [2]

 

 

 

 

동적 쿼리를 생성해보자

Predicate를 알아야 한다구요 

표현식을 작성할 수 있게 QueryDSL에서 제공하는 조건을 표현하는 인터페이스. SQL의 `WHERE`절에 해당하는 조건을 정의할 때 사용된다. 주로 필드 값에 대한 비교, 논리 연산 등을 표현하는데 사용된다. 

 

우리가 만들어야 할 동적 쿼리의 예시.

 

각 필터 요소를 설정하지 않을수도, 설정할 수도 있다. 이럴때 각 조건에 해당하는 쿼리문을 하나하나 만든다면 엄청난 양이 된다. 때문에 동적 쿼리로 만들어 작성하게 되는데, JPA로 builder 패턴 같이 쿼리문을 만들 수 있다! 

 

이때 사용하는 것이 BooleanBuilder인데, 이는 Predicate를 구현한 구현체다. BooleanBuilder를 이용해 조건절을 추가한 뒤 where절에 전달해서 동적으로 구현해 낼 수 있다.

 

import com.querydsl.core.BooleanBuilder;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class AccommodationRepositoryCustomImpl implements AccommodationRepositoryCustom {

    @Autowired
    private JPAQueryFactory jpaQueryFactory;

    public List<Accommodation> findAccommodations(Integer minBedrooms, Integer minBeds, Integer minBathrooms) {
        QAccommodation accommodation = QAccommodation.accommodation;
        BooleanBuilder builder = new BooleanBuilder();

        if (minBedrooms != null) {
            builder.and(accommodation.bedrooms.goe(minBedrooms));
        }

        if (minBeds != null) {
            builder.and(accommodation.beds.goe(minBeds));
        }

        if (minBathrooms != null) {
            builder.and(accommodation.bathrooms.goe(minBathrooms));
        }

        Predicate predicate = builder.getValue();

        return jpaQueryFactory.selectFrom(accommodation)
                              .where(predicate)
                              .fetch();
    }
}

 

 

QuerydslPredicateExcutor

JPA에서 QuerydslPredicateExcutor 인터페이스를 제공한다. 아래와 같이 레포지토리를 생성한다.

public interface QProductRepository extends JpaRepository<Product, Long>, QuerydslPredicateExcutor<Product>{

}

 

 

    @Test
    public void queryDSLTest1() {
        Predicate predicate = QProduct.product.name.containsIgnoreCase("펜")
            .and(QProduct.product.price.between(1000, 2500));

        Optional<Product> foundProduct = qProductRepository.findOne(predicate);

        if(foundProduct.isPresent()){
            Product product = foundProduct.get();
            System.out.println(product.getNumber());
            System.out.println(product.getName());
            System.out.println(product.getPrice());
            System.out.println(product.getStock());
        }
    }

 

 

앞선 BooleanBuilder는 매우 유연하고 복잡한 조건을 쉽게 결합할 수 있다. 하지만 직접 쿼리를 구성해야 해서 코드가 길어질 수 있다. QuerydlsPredicateExcutor는 레포지토리 인터페이스에 쉽게 통합이 가능하다. 

 

 

 

 

Reference


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

 

[Spring] QueryDsl gradle 설정 방법과 QueryDsl QClass 파일 생성 위치

spring data jpa 와 querydsl 을 같이 쓰는 경우 에러가 발생한 에러이다. build.gradle 파일 dependencies 에 에러 방지를 위한 아래 두 줄을 추가해주면 된다.스프링 부트의 3.0 버전으로 올라가면서 QueryDsl 패

velog.io

 

[2] https://weicomes.tistory.com/455

 

Spring Annotation 훑어보기.

@Configurable 도메인 객체는 bean이 아니지만, 도메인 객체에 DAO나 Repository를 주입시켜주어야 하는 경우가 있다. 하지만 Dependency Injection을 사용하려면 스프링이 관리하는 Bean 이여야 한다. 도메인 객

weicomes.tistory.com