[Spring Boot] QueryDSL 사용하기 (Spring Boot 3.x, Java 17 이후, Maven)

조성우·2024년 1월 24일
0

Spring Boot

목록 보기
3/12
post-thumbnail

QueryDSL

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


QueryDSL의 장점

  • IDE가 제공하는 코드 자동 완성 기능 사용 가능
  • 문법적으로 잘못된 쿼리를 허용하지 앟음
  • 고정된 SQL 쿼리를 작성하지 않기 때문에 동적으로 쿼리 생성
  • 코드로 작성하므로 가독성 및 생산성 향상
  • 도메인 타입과 프로퍼티를 안전하게 참조

프로젝트 설정

pom.xml (classifier를 jakarta로 명시해주어야 함)

		<!--QueryDSL을 사용하기 위한 디펜던시 추가-->
		<dependency>
			<groupId>com.querydsl</groupId>
			<artifactId>querydsl-jpa</artifactId>
			<version>5.0.0</version>
			<classifier>jakarta</classifier>
		</dependency>
		<dependency>
			<groupId>com.querydsl</groupId>
			<artifactId>querydsl-apt</artifactId>
			<version>5.0.0</version>
			<classifier>jakarta</classifier>
		</dependency>
		<dependency>
			<groupId>com.querydsl</groupId>
			<artifactId>querydsl-core</artifactId>
			<version>5.0.0</version>
		</dependency>

		...
		...
		...

		<!-- QueryDSL을 사용하기 위한 플러그인 추가 -->
		<plugin>
			<groupId>com.mysema.maven</groupId>
			<artifactId>apt-maven-plugin</artifactId>
			<version>1.1.3</version>
			<executions>
				<execution>
					<goals>
						<goal>process</goal>
					</goals>
					<configuration>
						<outputDirectory>target/generated-sources/java</outputDirectory>
						<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
					</configuration>
				</execution>
			</executions>
		</plugin>

APT(Annotation Processing Tool)는 어노테이션으로 정의된 코드를 기반으로 새로운 코드를 생성하는 기능임
위 코드에서 JPAAnnotationProcessor는 @Entity 어노테이션으로 정의된 엔티티 클래스를 찾아서 쿼리 타입을 생성함


이후 'Generate Sources ...' 실행

아래 경로에 Q도메인 클래스가 생성됨


QueryDSL 사용하기


우선 테스트 코드로 기본적인 QueryDSL 사용법을 알아보자

JPAQuery를 활용한 QueryDSL 테스트 코드

	// QueryDSL에 의해 생성된 Q도메인 클래스를 활용하는 코드
   
    @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();

        for (Product product : productList) {
            System.out.println("----------------");
            System.out.println();
            System.out.println("Product Number : " + product.getNumber());
            System.out.println("Product Name : " + product.getName());
            System.out.println("Product Price : " + product.getPrice());
            System.out.println("Product Stock : " + product.getStock());
            System.out.println();
            System.out.println("----------------");
        }
    }

반환 메서드 종류

  • List<T> fetch(): 조회 결과를 리스트로 반환
  • T fetchOne(): 단 건의 조회 결과 반환
  • T fetchFirst(): 여러 건의 조회 결과 중 1건을 반환 -> .limit(1).fetchOne()
  • Long fetchCount(): 조회 결과의 개수를 반환
  • QueryResult<T> fetchResults: 조회 결과 리스트와 개수를 포함한 QueryResults 반환

JPAQueryFactory를 활용한 QueryDSL 테스트 코드

	@Test
    void queryDslTest2() {
        JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
        QProduct qProduct = QProduct.product;

		// JPAQueryFactory에서는 select 절부터 작성 가능
        List<Product> productList = jpaQueryFactory.selectFrom(qProduct)
                .where(qProduct.name.eq("펜"))
                .orderBy(qProduct.price.asc())
                .fetch();

        for (Product product : productList) {
            System.out.println("----------------");
            System.out.println();
            System.out.println("Product Number : " + product.getNumber());
            System.out.println("Product Name : " + product.getName());
            System.out.println("Product Price : " + product.getPrice());
            System.out.println("Product Stock : " + product.getStock());
            System.out.println();
            System.out.println("----------------");
        }
    }
	@Test
    void queryDslTest3() {
        JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
        QProduct qProduct = QProduct.product;

		// 일부 컬럼 조회를 위해 select와 from 메서드 구분해서 사용
        List<String> productList = jpaQueryFactory
                .select(qProduct.name)
                .from(qProduct)
                .where(qProduct.name.eq("펜"))
                .orderBy(qProduct.price.asc())
                .fetch();

        for (String product : productList) {
            System.out.println("----------------");
            System.out.println("Product Name : " + product);
            System.out.println("----------------");
        }

		// 조회 대상이 여러 개일 경우 쉼표로 구분, 또한 리턴 타입을 List<Tuple>로 지정 (not List<String>)
        List<Tuple> tupleList = jpaQueryFactory
                .select(qProduct.name, qProduct.price)
                .from(qProduct)
                .where(qProduct.name.eq("펜"))
                .orderBy(qProduct.price.asc())
                .fetch();

        for (Tuple product : tupleList) {
            System.out.println("----------------");
            System.out.println("Product Name : " + product.get(qProduct.name));
            System.out.println("Product Name : " + product.get(qProduct.price));
            System.out.println("----------------");
        }
    }



QueryDSL을 실제 비즈니스 로직에서 활용하려면


QueryDSL Config 파일 생성

config/QueryDSLConfiguration.java

@Configuration
public class QueryDSLConfiguration {

    @PersistenceContext
    EntityManager entityManager;

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

}

위와 같이 JPAQueryFactory 객체를 @Bean 객체로 등록해두면 앞에서 작성한 예제처럼 매번 JPAQueryFactory를 초기화하지 않고 스프링 컨테이너에서 가져다 쓸 수 있음 (아래 코드 참고)

	// JPAQueryFactory 빈을 활용한 테스트 코드
	@Autowired
    JPAQueryFactory jpaQueryFactory;

    @Test
    void queryDslTest4() {
        QProduct qProduct = QProduct.product;

        List<String> productList = jpaQueryFactory
                .select(qProduct.name)
                .from(qProduct)
                .where(qProduct.name.eq("펜"))
                .orderBy(qProduct.price.asc())
                .fetch();

        for (String product : productList) {
            System.out.println("----------------");
            System.out.println("Product Name : " + product);
            System.out.println("----------------");
        }
    }


QuerydslPredicateExecutor, QuerydslRepositorySupport 활용

스프링 데이터 JPA에서 QueryDSL을 더 편하게 사용할수 있도록 QuerydslPredicateExecutor 인터페이스와 QuerydslRepositorySupport 클래스를 제공함


QuerydslPredicateExecutor 인터페이스

JPARepository와 함께 리포지토리에서 QueryDSL을 사용할 수 있게 인터페이스를 제공함


QuerydslPredicateExecutor를 사용하는 리포지토리(QProductRepository) 생성

public interface QProductRepository extends JpaRepository<Product, Long>,
    QuerydslPredicateExecutor<Product> {
}
  • QuerydslPredicateExecutor에서 제공하는 메서드

    Predicate는 표현식을 작성할 수 있게 QueryDSL에서 제공하는 인터페이스임

QProductRepositoryTest 클래스 생성 및 테스트 코드 작성(활용)

@SpringBootTest
public class QProductRepositoryTest {

    @Autowired
    QProductRepository qProductRepository;

    @Test
    public void queryDSLTest1() {  // findOne()
    	// Predicate 명시적으로 정의함
        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());
        }
    }
    
    @Test
    public void queryDSLTest2() {  // findAll()
        QProduct qProduct = QProduct.product;

		// Predicate 서술부만 가져다 사용함
        Iterable<Product> productList = qProductRepository.findAll(
            qProduct.name.contains("펜")
                .and(qProduct.price.between(550, 1500))
        );

        for (Product product : productList) {
            System.out.println(product.getNumber());
            System.out.println(product.getName());
            System.out.println(product.getPrice());
            System.out.println(product.getStock());
        }
    }
}

QueryDSL을 더욱 편하게 사용할 수 있지만, join이나 fetch 기능은 사용할 수 없다는 단점이 있음


QuerydslRepositorySupport 추상 클래스 사용하기

가장 보편적으로 사용하는 방식인 CustomRepository를 활용해 리포지토리를 구현해보자


위에서 예로 든 Product 엔티티를 활용하기 위한 객체들의 상속구조는 아래와 같음

JpaRepository와 QuerydslRepositorySupport는 Spring Data JPA에서 제공하는 인터페이스와 클래스이며, 나머지는 직접 구현해야 함

  • 먼저 JpaRepository를 상속받는 ProductRepository 생성
  • 이때 직접 구현한 쿼리를 사용하기 위해서는 JpaRepository를 상속받지 않는 리포지토리 인터페이스인 ProductRepositoryCustom을 생성함 (여기서 정의하고자 하는 기능을 메서드로 정의)
  • ProductRepositoryCustom에서 정의한 메서드를 사용하기 위해 ProductRepository에서 ProductRepositoryCustom을 상속받음
  • ProductRepositoryCustom에서 정의된 메서드를 기반으로 실제 쿼리 작성을 하기 위한 구현체인 ProductRepositoryCustomImpl 클래스를 생성함
  • ProductRepositoryCustomImpl 클래스에서는 다양한 방법으로 쿼리를 구현할 수 있지만 QueryDSL을 사용하기 위해 QuerydslRepositorySupport를 상속받음

위와 같이 구성하면 DAO나 서비스에서 리포지토리에 접근하기 위해 ProductRepository를 사용하며 따라서 QueryDSL의 기능도 사용할 수 있게 됨


repository/support/ProductRepositoryCustom.java

// 필요한 쿼리를 작성할 메소드를 정의하는 인터페이스
public interface ProductRepositoryCustom {

    List<Product> findByName(String name);

}

repository/support/ProductRepositoryCustomImpl.java

@Component
public class ProductRepositoryCustomImpl extends QuerydslRepositorySupport implements
    ProductRepositoryCustom {

    // 생성자를 통해 도메인 클래스를 부모 클래스에 전달애햐 함
    public ProductRepositoryCustomImpl() {
        super(Product.class);
    }

    // 인터페이스에 정의한 메서드를 구현
    @Override
    public List<Product> findByName(String name) {
        QProduct product = QProduct.product;

        // Q도메인 클래스를 사용해 QuerydslRepositorySupport가 제공하는 기능 사용
        List<Product> productList = from(product)
            .where(product.name.eq(name))
            .select(product)
            .fetch();

        return productList;
    }
}

repository/support/ProductRepository.java
(기존 Product 엔티티 클래스와 매핑해서 사용하던 ProductRepository 사용 가능하나 예시를 위해 별도로 생성함)

import com.springboot.advanced_jpa.data.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

// repository.ProductRepository와 이름이 동일하여 Bean 생성 충돌이 발생하므로 Bean 이름을 지정함
@Repository("productRepositorySupport")
public interface ProductRepository extends JpaRepository<Product, Long>, ProductRepositoryCustom {

}

findByName() 메서드 테스트 수행

위 코드를 사용할 때는 ProductRepository만 이용하면 됨
ProductRepositoryCustom에서 정의한 findByName()을 테스트해보자

@SpringBootTest
public class ProductRepositoryTest {
    @Autowired
    ProductRepository productRepository;

    @Test
    void findByNameTest(){
        List<Product> productList = productRepository.findByName("펜");

        for(Product product : productList){
            System.out.println(product.getNumber());
            System.out.println(product.getName());
            System.out.println(product.getPrice());
            System.out.println(product.getStock());
        }
    }
}

리포지토리를 구성하면서 모든 로직을 구현했기 때문에 findByName() 메서드를 사용할 때는 위와 같이 간단하게 구현해 사용할 수 있음

0개의 댓글