22.11.17
7. 데이터 접근 기술 - Querydsl
Querydsl 소개1 - 기존 방식의 문제점
버그 발생
String sql =
"select * from member" +
"where name like ?" +
"and age between ? and ?";
-> select * from memberwhere name like ?and age between ? and ?
Query 의 문제점
- Query 는 문자, Type-check 불가능
- 실행하기 전까지 작동여부 확인 불가
=> 컴파일 에러X, 런타임 에러O
런타임 에러 중에서도 서버를 띄우면 나는 에러와 고객이 그 기능을 호출할 때 나는 에러가 있다.
- 컬럼명을 직접 기입해야 한다.
만약 SQL이 클래스처럼 타입이 있고 자바 코드로 작성할 수 있다면?
Type-safe
컴파일시 에러 체크 가능
code assistant 사용 가능
Querydsl
쿼리를 자바로 type-safe 하게 개발할 수 있게 지원하는 프레임워크
주로 JPA 쿼리(JPQL)에 사용
JPA에서 쿼리 방법
- JPQL(HQL)
- Criteria API
- MetaModel Criteria API (type-safe)

- 장점: SQL 쿼리와 비슷해서 금방 익숙해진다.
- 단점: type-safe 가 아니다. 동적 쿼리 생성이 어렵다.

실행되는 SQL
select id, age, name
from MEMBER
where age between 20 and 40 and name like '김%'
order by age desc limit 3
- 장점: 동적 쿼리 생성이 쉽다??
- 단점: type safe가 아니다. 코드가 복잡하고 알아야 할 것이 많다.
3. MetaModel Criteria API (type-safe)
- root.get("age") -> root.get(Member_.age)
- Criteria API + MetaModel
- Criteria API 와 거의 동일, 복잡하다.
- type-safe
Querydsl 소개2 - 해결
QueryDSL 분석
Domain (도메인)
Specific (특화)
Language (언어)
DSL 은 특정한 도메인에 초점을 맞춘 제한적인 표현력을 가진 컴퓨터 프로그래밍 언어이다.
QueryDSL 은 쿼리에 특화된 프로그래밍 언어로 다양한 저장소 쿼리 기능을 통합한다.
특징: 단순, 간결, 유창

Type-safe Query Type 생성

APT(Annotation Processing Tool) 가 JPA 엔티티(@Entity)를 읽어서 쿼리용 도메인을 생성한다.
Querydsl - JPA

실행되는 SQL
select id, age, name
from MEMBER
where age between 20 and 40 and name like '김%'
order by age desc limit 3
- 작동 방식: Querydsl -> JPQL -> SQL
- Querydsl 은 JPQL 을 생성하는 빌더
- 장점: type-safe, 단순하고 쉽다.
- 단점: Q코드 생성을 위한 APT를 설정해야 한다.
구성
- Query: 예) from, where, join...
- Path: 예) QMember, QMember.name...
- Expession: 예) name.eq, name.gt...
단순 쿼리, 동적 쿼리, 조인 쿼리, 페이징, 정렬 모두 지원
SpringDataJPA - Querydsl
- SpringData 프로젝트의 약점은 조회 (복잡한 쿼리, 동적 쿼리)
- Querydsl 로 조회 기능 보완
- 단순한 경우 -> SpringDataJPA
- 복잡한 경우 -> Querydsl
Querydsl 정리
- type-safe ( 컴파일 에러, IDE 의 Code-assistant )
- 자바 코드로 쿼리 작성
- 결국 JPQL 빌더이기 때문에, JPQL 로 해결하기 어려운 복잡한 쿼리는 네이티브 SQL 쿼리를 사용 (JdbcTemplate, MyBatis)
Querydsl 설정
build.gradle
dependencies {
//Querydsl 추가
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"
}
//Querydsl 추가, 자동 생성된 Q클래스 gradle clean으로 제거
clean {
delete file('src/main/generated')
}
참고: Querydsl 은 버전이나 환경에 따라 설정하는 것이 약간씩 다를 수 있다. 검색을 통해 해결하자.
검증 - Q 타입 생성 확인 방법
Preferences Build, Execution, Deployment Build Tools Gradle

Build and run 옵션은 둘 다 같게 맞추어 두자.
- Gradle: Gradle을 통해서 빌드한다.
- IntelliJ IDEA: IntelliJ가 직접 자바를 실행해서 빌드한다.
옵션 선택1 - Gradle
1. Q타입 생성
Gradle -> Tasks -> build -> clean
Gradle -> Tasks -> other -> compileJava

2. Q타입 생성 확인
build -> generated -> sources -> annotationProcessor -> java/main -> hello.itemservice.domain.QItem

3. Q타입 삭제
gradle clean 을 수행하면 build 폴더 자체가 삭제된다. 따라서 별도의 설정은 없어도 된다.
옵션 선택2 - IntelliJ IDEA
1. Q타입 생성
Build -> Build Project 또는
Build -> Rebuild 또는
main() , 또는 테스트를 실행하면 된다.
2. Q타입 생성 확인
src/main/generated -> hello.itemservice.domain.QItem

참고: Q타입은 컴파일 시점에 자동 생성되므로 버전관리(GIT)에 포함하지 않는 것이 좋다.
gradle 옵션을 선택하면 Q타입은 gradle build 폴더 아래에 생성되는데, 대부분 gradle build 폴더를 git에 포함하지 않기 때문에 이 부분은 자연스럽게 해결된다.
IntelliJ IDEA 옵션을 선택하면 Q타입은 src/main/generated 폴더 아래에 생성되기 때문에 여기를 포함하지 않는 것이 좋다. (git ignore)
3. Q타입 삭제
//Querydsl 추가, 자동 생성된 Q클래스 gradle clean으로 제거
clean {
delete file('src/main/generated')
}
IntelliJ IDEA 옵션을 선택하면 src/main/generated 에 파일이 생성되고, 필요한 경우 Q파일을 직접 삭제해야 한다.
gradle 에 해당 스크립트를 추가하면 gradle clean 명령어를 실행할 때 src/main/generated 의 파일도 함께 삭제해준다.
참고: Querydsl은 이렇게 설정하는 부분이 사용하면서 조금 귀찮은 부분인데, IntelliJ가 버전업 하거나 Querydsl의 Gradle 설정이 버전업 하면서 적용 방법이 조금씩 달라지기도 한다. 그리고 환경에 따라서 잘 동작하지 않기도 한다.
공식 메뉴얼에 소개 되어 있는 부분이 아니기 때문에, 설정에 수고로움이 있지만 querydsl gradle 로 검색하면 자신의 환경에 맞는 대안을 금방 찾을 수 있을 것이다.
Querydsl 적용
JpaItemRepositoryV3 클래스
기본적으로 JPA 사용, 동적 쿼리( findAll() ) 에서 Querydsl 을 사용했다.
@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);
}
@Override
public Item save(Item item) {
em.persist(item);
return item;
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
Item findItem = em.find(Item.class, itemId);
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
@Override
public Optional<Item> findById(Long id) {
Item item = em.find(Item.class, id);
return Optional.ofNullable(item);
}
//QueryDSL 사용, 동적 쿼리
@Override
public List<Item> findAll(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
BooleanBuilder builder = new BooleanBuilder();
if(StringUtils.hasText(itemName)){
builder.and(item.itemName.like(itemName));
}
if(maxPrice != null){
builder.and(item.price.loe(maxPrice)); //maxPrice 보다 작거나 같다
}
List<Item> result = query
.select(item)
.from(item)
.where(builder)
.fetch();
return result;
}
}
Querydsl을 사용하려면 JPAQueryFactory 가 필요하다. JPAQueryFactory 는 JPA 쿼리인 JPQL을 만들기 때문에 EntityManager 가 필요하다.
설정 방식은 JdbcTemplate 을 설정하는 것과 유사하다. 참고로 JPAQueryFactory 를 스프링 빈으로 등록해서 사용해도 된다.
QuerydslConfig 설정 파일
@Configuration
public class QuerydslConfig {
private final EntityManager em;
public QuerydslConfig(EntityManager em) {
this.em = em;
}
@Bean
public ItemService itemService(){
return new ItemServiceV1(itemRepository());
}
@Bean
public ItemRepository itemRepository(){
return new JpaItemRepositoryV3(em);
}
}
- findAll() : Querydsl을 사용해서 동적 쿼리 문제를 해결한다. BooleanBuilder 를 사용해서 원하는 where 조건들을 넣어주면 된다. 이 모든 것을 자바 코드로 작성하기 때문에 동적 쿼리를 매우 편리하게 작성할 수 있다.
ItemServiceApplication - 변경
@Import(QuerydslConfig.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {}
예외 변환
Querydsl 은 별도의 스프링 예외 추상화를 지원하지 않는다.
대신에 JPA에서 학습한 것 처럼 @Repository 에서 스프링 예외 추상화를 처리해준다.
findAll() 리팩토링
기존의 findAll() 은 findAllOld() 로 변경
@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 BooleanExpression likeItemName(String itemName){
if(StringUtils.hasText(itemName)){
return item.itemName.like("%" + itemName + "%");
}
return null;
}
private BooleanExpression maxPrice(Integer maxPrice){
if(maxPrice != null){
return item.price.loe(maxPrice); //maxPrice 보다 작거나 같다
}
return null;
}
- Querydsl에서 where(A,B) 에 다양한 조건들을 직접 넣을 수 있는데, 이렇게 넣으면 AND 조건으로 처리된다.
- where() 에 null 을 입력하면 해당 조건은 무시한다. 따라서 조건에 해당하지 않으면 null 을 반환하도록 했다.
- likeItemName() , maxPrice() 를 다른 쿼리를 작성할 때 재사용 할 수 있다. -> 쿼리 조건 모듈화 (자바 코드의 이점)
정리
Querydsl 장점
- 동적 쿼리를 매우 깔끔하게 사용할 수 있다.
- 쿼리 문장에 오타가 있어도 컴파일 시점에 오류를 막을 수 있다.
- 메서드 추출을 통해서 코드를 재사용할 수 있다.
Querydsl은 이 외에도 수 많은 편리한 기능을 제공한다.
예를 들어서 최적의 쿼리 결과를 만들기 위해서 DTO로 편리하게 조회하는 기능은 실무에서 자주 사용하는 기능이다.
JPA를 사용한다면 스프링 데이터 JPA와 Querydsl은 실무의 다양한 문제를 편리하게 해결하기 위해 선택하는 기본 기술이다.
Querydsl 은 JPQL 의 빌더 역할일 뿐, JPA에 대한 이해가 선행되어야 한다.