메서드의 이름을 기반으로 생성하는 JPQL의 한계는 @Query 어노테이션을 통해 대부분 해소할 수 있지만 직접 문자열을 입력하기 때문에 컴파일 시점에 에러를 잡지 못하고 런타임 에러가 발생할 수 있음.
쿼리의 문자열이 잘못된 경우에는 애플리케이션이 실행된 후 로직이 실행되고 나서야 오류 발견.
이러한 이유로 개발 환경에서는 문제가 없어 보이다가 실제 운영 환경에 애플리케이션을 배포하고 나서 오류가 발견되는 리스크 유발.
이 같은 문제 해결을 위해 QueryDSL 사용.
QueryDSL은 문자열이 아닌 코드로 쿼리 작성을 도와줌.
정적 타입을 이용해 SQL과 같은 쿼리를 생성할 수 있도록 지원하는 프레임워크. 문자열이나 XML 파일을 통해 쿼리를 작성하는 대신 QueryDSL이 제공하는 Fluent API를 활용해 쿼리 생성.
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>
APT란?
Annotation Processing Tool
어노테이션으로 정의된 코드를 기반으로 새로운 코드를 생성하는 기능.
클래스를 컴파일하는 기능도 제공.
3. 메이븐의 compile 단계를 클릭해 빌드 작업 수행
빌드가 완료되면 위에서 작성했던 outputDirectory에 설정한 generated-source 경로에 다음과 같이 Q도메인 클래스가 생성된 것을 볼 수 있음.
4. IntelliJ에서 [File] -> [Project Structure]
[Mark as]의 [Sources] 눌러 IDE에서 소스파일로 인식할 수 있게 설정
QueryDSL 적용 완료❗
QueryDSL은 지금까지 작성했던 엔티티 클래스와 Q도메인이라는 쿼리 타입의 클래스를 자체적으로 생성해서 메타데이터로 사용. 이를 통해 SQL과 같은 쿼리를 생성해 제공
@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 타입으로 값을 리턴받기 위해서는 fetch()
메서드 사용
.limit(1).fetchOne()
@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("----------------");
}
}
만약 전체 컬럼을 조회하지 않고 일부만 조회하고 싶다면 다음과 같이 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("----------------");
}
}
@Configuration
public class QueryDSLConfiguration {
@PersistenceContext
EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
JPAQueryFactory 객체를 @Bean 객체로 등록해두면 매번 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("----------------");
}
}
스프링 데이터 JPA에서는 QueryDSL을 더욱 편하게 사용할 수 있게 QuerydslPredicateExecutor 인터페이스와 QuerydslRepositorySupport 클래스 제공
JpaRepository와 함께 리포지토리에서 QueryDSL을 사용할 수 있게 인터페이스 제공
QuerydslPredicateExecutor를 사용하는 리포지토리 생성
public interface QProductRepository extends JpaRepository<Product, Long>, QuerydslPredicateExecutor<Product> {
}
QuerydslPredicateExecutor에서 제공하는 메서드
대부분 Predicate 타입을 매개변수로 받음.
Predicate는 표현식을 작성할 수 있게 QueryDSL에서 제공하는 인터페이스.
@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
가장 보편적으로 사용하는 방식은 CustomRepository를 활용해 리포지토리를 구현하는 방식
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 {
}
@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()
메서드를 사용할 때는 위와 같이 간단히 구현 가능
출처 - (책) 스프링 부트 핵심 가이드 / 장정우, 위키북스