[Spring] QueryDSL 적용 및 활용하기

Miin·2023년 11월 20일
0

Spring

목록 보기
11/17

QueryDSL 적용하기

메서드의 이름을 기반으로 생성하는 JPQL의 한계는 @Query 어노테이션을 통해 대부분 해소할 수 있지만 직접 문자열을 입력하기 때문에 컴파일 시점에 에러를 잡지 못하고 런타임 에러가 발생할 수 있음.
쿼리의 문자열이 잘못된 경우에는 애플리케이션이 실행된 후 로직이 실행되고 나서야 오류 발견.

이러한 이유로 개발 환경에서는 문제가 없어 보이다가 실제 운영 환경에 애플리케이션을 배포하고 나서 오류가 발견되는 리스크 유발.

이 같은 문제 해결을 위해 QueryDSL 사용.
QueryDSL은 문자열이 아닌 코드로 쿼리 작성을 도와줌.

QueryDSL이란?

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

QueryDSL 장점

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

pom.xml

1. 의존성 추가

		<dependency>
			<groupId>com.querydsl</groupId>
			<artifactId>querydsl-apt</artifactId>
			<scope>provided</scope>
		</dependency>

		<dependency>
			<groupId>com.querydsl</groupId>
			<artifactId>querydsl-jpa</artifactId>
		</dependency>

2. APT 플러그인 추가

			<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>
							<options>
								<querydsl.entityAccessors>true</querydsl.entityAccessors>
							</options>
						</configuration>
					</execution>
				</executions>
			</plugin>
  • JPAAnnotationProcessor는 @Entity 어노테이션으로 정의된 엔티티 클래스를 찾아서 쿼리 타입 생성

APT란?

Annotation Processing Tool
어노테이션으로 정의된 코드를 기반으로 새로운 코드를 생성하는 기능.
클래스를 컴파일하는 기능도 제공.


3. 메이븐의 compile 단계를 클릭해 빌드 작업 수행

빌드가 완료되면 위에서 작성했던 outputDirectory에 설정한 generated-source 경로에 다음과 같이 Q도메인 클래스가 생성된 것을 볼 수 있음.

4. IntelliJ에서 [File] -> [Project Structure]
[Mark as]의 [Sources] 눌러 IDE에서 소스파일로 인식할 수 있게 설정

QueryDSL 적용 완료❗


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



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("----------------");
        }
    }
  • QueryDSL을 사용하기 위해서는 JPAQuery 객체 사용
  • JPAQuery는 엔티티 매니저(EntityManager)를 활용해 생성
  • JPAQuery는 빌더 형식으로 쿼리 작성

List 타입으로 값을 리턴받기 위해서는 fetch() 메서드 사용

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

JPAQueryFactory 활용

	@Test
    void queryDslTest2() {
        JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
        QProduct qProduct = QProduct.product;
        
        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("----------------");
        }
    }
  • JPAQueryFactory를 사용하면 select절부터 작성 가능

만약 전체 컬럼을 조회하지 않고 일부만 조회하고 싶다면 다음과 같이 selectFrom()이 아닌 select()from() 메서드를 구분해서 사용하면 됨.

	@Test
    void queryDslTest3() {
        JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
        QProduct qProduct = QProduct.product;
        
        // select 대상이 하나인 경우
        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("----------------");
        }
        
        // select 대상이 여러 개일 경우 (,)로 구분해서 작성
        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을 실제 비즈니스 로직에서 활용

컨피그 클래스 생성

@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를 사용하는 리포지토리 생성

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

}

QuerydslPredicateExecutor에서 제공하는 메서드

  • Optional<T.> findOne(Predicate predicate);
  • Iterable<T.> findAll(Predicate predicate);
  • Iterable<T.> findAll(Predicate predicate, Sort sort);
  • Iterable<T.> findAll(Predicate predicate, OrderSpecifier<?>... orders);
  • Iterable<T.> findAll(OrderSpecifier<?>...orders);
  • Page<T.> findAll(Predicate predicate, Pageable pageable);
  • long count(Predicate predicate);
  • boolean exists(Predicate predicate);

대부분 Predicate 타입을 매개변수로 받음.
Predicate는 표현식을 작성할 수 있게 QueryDSL에서 제공하는 인터페이스.


Predicate를 활용한 findOne() 메서드 호출 테스트 코드

@SpringBootTest
public class QProductRepositoryTest {

    @Autowired
    QProductRepository qProductRepository;

    @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());
        }
    }

}

Predicate는 간단하게 표현식으로 정의하는 쿼리.
위는 Predicate를 명시적으로 정의하고 사용했지만 다음과 같이 서술부만 가져다 사용할 수도 있음.

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

        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());
        }
    }

QuerydslPredicateExecutor를 활용하면 더욱 편하게 QueryDSL을 사용할 수 있지만 join이나 fetch 기능은 사용할 수 없는 단점 O


QueryRepositorySupport 추상 클래스 사용하기

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

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


예시 코드

ProductRepositoryCustom
인터페이스를 생성하고 쿼리로 구현하고자 하는 메서드 정의

public interface ProductRepositoryCustom {

    List<Product> findByName(String name);

}

ProductRepositoryCustom 인터페이스의 구현체인 ProductRepositoryCustomImpl 클래스 생성

QueryDSL 사용을 위해 QuerydslRepositorySupport를 상속받고 ProductRepositoryCustom 인터페이스 구현. QuerydslRepositorySupport를 상속받으면 생성자를 통해 도메인 클래스를 부모 클래스에 전달해야 함.
인터페이스에 정의한 메서드 구현.

@Component
public class ProductRepositoryCustomImpl extends QuerydslRepositorySupport implements ProductRepositoryCustom {

    public ProductRepositoryCustomImpl() {
        super(Product.class);
    }

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

        List<Product> productList = from(product)
                .where(product.name.eq(name))
                .select(product)
                .fetch();

        return productList;
    }
}

ProductRepository 인터페이스

기존에 리포지토리를 생성하는 것과 동일하게 JpaRepository 상속받음.

기본적으로 JpaRepository에서 제공하는 메서드도 사용할 수 있고, 별도로 ProductRepositoryCustom 인터페이스에서 정의한 메서드도 구현체를 통해 사용 가능.

@Repository("productRepositorySupport")
public interface ProductRepository extends JpaRepository<Product, Long>, ProductRepositoryCustom {
    
}

ProductRepository의 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() 메서드를 사용할 때는 위와 같이 간단히 구현 가능




출처 - (책) 스프링 부트 핵심 가이드 / 장정우, 위키북스

profile
컴퓨터공학전공 학부생 Back-end Developer

0개의 댓글