Home Querydsl
Post
Cancel

Querydsl

쿼리는 문자이다. Type-check가 불가능하다. 즉 실행하기 전까지 작동 여부를 확인할 수 없다. 단순히 띄어쓰기를 하나 틀려도 컴파일러에서 잡아주지 못하고, 에러를 찾기 힘든 것이다.

에러는 크게 2가지이다.

  • 컴파일 에러 (좋은 에러)
  • 런타임 에러 (나쁜 에러)

즉, 기존의 query 방식은 나쁜 에러인 것이다.

더해 SQL을 작성하기 위해서는 도메인과 연관된 테이블의 모든 컬럼을 알아야 한다. 이것도 할 수는 있지만 어렵고 번거롭다.

만약 SQL이 클래스처럼 타입이 있고, 자바코드로 작성할 수 있다면?

컴파일 시 에러도 체크 가능하고, Code-assistant도 사용가능하는 등 매우 편리하다.

이런 것을 편리하게 해주는 것이 바로 QueryDSL이다. 쿼리를 Java로 type-safe하게 개발할 수 있도록 지원해준다. 주로 JPA 쿼리 (JPQL)에 사용한다.

기존에 어떤 문제가 있길래 QueryDSL이 나왔을까.

JPA에서 QUERY 방법은 크게 3가지가 있다.

  • JPQL
  • Criteria API
  • MetaModel Criteria API(type-safe)

각각의 장단점에 대해 알아보자.

  • JPQL
    • 장점: SQL Query와 비슷해 금방 익숙해짐
    • 단점: type-safe 아님, 동적 쿼리 생성이 어려움
  • Criteria API
    • 장점: 동적 쿼리 생성이 쉬움 (?) 이 또한 애매.
    • 단점: type-safe 아님, 매우 매우 복잡함, 알아야 할게 너무 많음
  • MetaModel Criteria API
    • 장점: 동적 쿼리 생성이 쉬움, type-safe임. Criteria API와 비슷함.
    • 단점: 매우 복잡함.

3가지 방법 모두 문제가 있었다. type-safe한 것을 쓰고 싶은데 결국 너무 복잡해 쓰기 어렵고, 그렇다고 쉬운 JPQL은 동적 쿼리 작성도 어렵고, tpye-safe하지도 않다.

기존 방법들이 위와 같이 간단하게는 일부는 동적 쿼리 문제, 일부는 너무 복잡하다는 문제가 있어 QueryDSL이 나오게 되었다.

QueryDSL

DSL (Domain Specific Language) => 도메인 특화 언어이다.

특정한 도메인에 초점을 맞춘 제한적인 표현력을 가진 컴퓨터 프로그래밍 언어이다.

특징은 간순하고 간결하며 유창하다. 다양한 저장소 쿼리 기능을 통합해놓았다.

Spring Data가 기본 CRUD같은 기능을 추상화해보겠다고 나온 것이라면 QueryDSL은 다양한 기술들에 있어 쿼리 기능을 추상화해보겠다고 나온 것이다.

Querydsl은 그중에서도 JPA 쿼리(JPQL)을 type-safe하게 작성하는데 많이 사용된다.

동작 방식

엔티티(@Entity, Member)를 보고 QueryDSL이 APT(Annotation Processing Tool)로 Querydsl이 사용하는 새로운 객체(QMember)를 생성한다.

그 객체로 Querydsl 코드를 짜면 JPQL이 생성되고, 그 JPQL로 JPA가 실행되면 SQL을 생성하고 실행되는 것이다.

JPQL을 잘 알아야 잘 사용할 수 있다.

장단점

  • 장점
    • type-safe
    • 단순하고 쉽다.
  • 단점
    • Q코드 생성을 위한 APT를 설정해야 한다.

SpringDataJPA + Querydsl

  • SpringData 프로젝트의 약점은 조회이다.
  • Querydsl로 복잡한 조회 기능을 보완할 수 있다.
    • 복잡한 쿼리
    • 동적 쿼리

단순한 경우에는 SpringData, 복잡한 경우 Querydsl을 사용할 수 있다.

QueryDSL 적용

설정

build.gradle 추가.

1
2
3
4
5
6
7
8
9
10
dependencies {
	implementation 'com.querydsl:querydsl-jpa' 
	annotationProcessor "com.querydsl:querydsl-apt:$ {dependencyManagement.importedProperties['querydsl.version']}:jpa" 
	annotationProcessor "jakarta.annotation:jakarta.annotation-api" 
	annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

clean {
	delete file('src/main/generated')
}
  • clean: 자동 생성된 Q클래스 gradle clean으로 제거한다.

Q타입 생성 확인

IntelliJ setting에서 빌드 방식을 IntelliJ와 Gradle 2가지 옵션을 선택할 수 있다.

  • Gradle 선택 시 Q타입 생성 확인 방법
  • Gradle -> Tasks -> build -> clean
  • Gradle -> tasks -> other -> compileJava

위와 같이 더블클릭해서 실행해주면

build -> generated -> sources -> anootationProcessor -> java/main 하위에 현재 프로젝트의 도메인 중 @Entity가 붙은 QItem 클래스가 생성되는 것을 볼 수 있다.

*참고: Q타입은 컴파일 시점에 자동 생성되므로 버전관리(GIT)에 포함하지 않는 것이 좋다. gradle 옵션을 선택하면 Q타입은 gradle build 폴더 아래에 생성되기 때문에 이 부분을 포함하지 않아야 한다. 대부분 gradle build 폴더를 git에 포함하지 않기 때문에 이 부분은 자연스럽게 해결된다.

Q타입을 삭제하려면 build clean을 수행한다. build 폴더 자체가 삭제된다.

  • IntelliJ 옵션 Q타입 생성 확인
    • Build -> Build Project 또는 Build -> Rebuild 또는 main 실행 또는 테스트 실행

src/main/generated 하위에 QItem이 생성된다.

*참고: 이 경우 GIT에 포함하지 않는 경로는 src/main/generated 폴더일 것이다.

Q타입을 삭제하려면 직접 삭제해주거나 gradle에 추가했던 clean 부분이 있다면, gradle clean 명렁어를 실행할 때 삭제해준다.

적용 (JPA + Querydsl)

Repository

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Repository  
@Transactional  
public class JpaItemRepositoryV3 implements ItemRepository {  
  
	private final EntityManager em;  
	private final JPAQueryFactory query;  
	  
	public JpaItemRepositoryV3(EntityManager em) {  
		this.em = em;  
		this.query = new JPAQueryFactory(em);  
	}  
	  
	//save(), update(), findById() ..
	  
	@Override  
	public List<Item> findAll(ItemSearchCond cond) {  
		String itemName = cond.getItemName();  
		Integer maxPrice = cond.getMaxPrice();  
		  
		QItem item = new QItem("i");  
		BooleanBuilder builder = new BooleanBuilder();  
		  
		if (StringUtils.hasText(itemName)) {  
			builder.and(item.itemName.like("%" + itemName + "%"));  
		}  
		if (maxPrice != null) {  
			builder.and(item.price.loe(maxPrice));  
		}  
		  
		return query.select(item)  
				.from(item)  
				.where()  
				.fetch();  
	}  
}

동적 쿼리를 사용하는 findAll()을 제외하면 나머지 메서드는 JPA를 사용한 것이다.

  • Querydsl을 사용하려면 JpaQueryFactory가 필요하다. JPA 쿼리인 JPQL을 만드는 역할을 하기 때문에 EntityManager가 필요하다.
    • JpaQueryFactory를 스프링 빈으로 등록해서 사용해도 된다.

findAll()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override  
public List<Item> findAll(ItemSearchCond cond) {  
	String itemName = cond.getItemName();  
	Integer maxPrice = cond.getMaxPrice();  
	  
	return query.select(item)  
			.from(item)  
			.where(likeItemName(itemName), maxPrice(maxPrice))  
			.fetch();  
}  
  
private Predicate maxPrice(Integer maxPrice) {  
	if (maxPrice != null) {  
		return QItem.item.price.loe(maxPrice);  
	}  
	return null;  
}  
  
private BooleanExpression likeItemName(String itemName) {  
	if (StringUtils.hasText(itemName)) {  
		return QItem.item.itemName.like("%" + itemName + "%");  
	}  
	return null;  
}

기존 코드를 이렇게 리팩토링 할 수도 있다. 이 방법이 더 낫다. 참고로 QItem은 static import를 이용해 QItem.item을 item으로 수정할 수 있다.

  • Querydsl에서 where(A,B)에 다양한 조건들을 직접 넣을 수 있는데 위와 같이 넣으면 AND 조건으로 처리된다. where()에 null이 들어가게 되면 해당 조건은 무시된다.
  • 위와 같이 리팩토링하면 likeItemName(), maxPrice()를 다른 쿼리를 작성할 때 재사용할 수 있다는 장점이 생긴다.

동적쿼리 작성이 매우매우매우 편해지고 간결하고 유지보수 측면에서도 훨씬 좋아진 것이 보인다.

정리

Querydsl을 사용함으로써 동적 쿼리를 매우 깔끔하게 사용할 수 있었다.

  • 쿼리 문장에 오타가 있어도 컴파일 시점에 오류를 막을 수 있다.
  • 메서드 추출을 통해 코드를 재사용할 수 있다.

Querydsl은 이 외에도 수 많은 편리한 기능을 제공한다. 예를 들면 최적의 쿼리 결과를 만들기 위해 DTO로 편리하게 조회하는 기능은 실무에서 자주 사용하는 기술이다. 이러한 기능도 제공한다.

JPA를 사용한다면 Spring Data JPA와 Querydsl은 실무의 다양한 문제를 편리하게 해결하기 위해 선택하는 기본 기술이다.

This post is licensed under CC BY 4.0 by the author.