QueryDsl의 탄생 이유
QueryDsl은 뭐고 왜 생겼을까? 기존 JPA를 사용할 때 다 좋은데 아쉬운 점은 동적쿼리에 취약하다는 점이다. JDBC도 마찬가지로 동적 쿼리가 매우 불편하다. 둘 다 동적으로 쿼리를 만드려면 코드가 매우 지저분해진다. 다음 코드를 보자.
@Override
public List<Item> findAll(ItemSearchCond cond) {
String jpql = "SELECT i FROM Item i";
Integer maxPrice = cond.getMaxPrice();
String itemName = cond.getItemName();
if (StringUtils.hasText(itemName) || maxPrice != null) {
jpql += " where";
}
boolean andFlag = false;
if (StringUtils.hasText(itemName)) {
jpql += " i.itemName like concat('%',:itemName,'%')";
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) {
jpql += " and";
}
jpql += " i.price <= :maxPrice";
}
log.info("jpql={}", jpql);
TypedQuery<Item> query = em.createQuery(jpql, Item.class);
if (StringUtils.hasText(itemName)) {
query.setParameter("itemName", itemName);
}
if (maxPrice != null) {
query.setParameter("maxPrice", maxPrice);
}
return query.getResultList();
}
- 이건 JPA로 동적 쿼리를 작성하는 부분이다. 보기만 해도 그래서 결국 어떤 쿼리를 날릴까?에 대한 굉장한 의문이 생긴다.
- 그리고 더 큰 문제는 동적 쿼리를 만들어 낼 때 문자열에 문자열을 추가하는데 이게 자칫 잘못했다가는 SQL에 문제가 생길 수도 있다. 다음 코드를 보자.
String sql =
"select * from member" +
"where name like ?" +
"and age between ? and ?"
- 자칫 보면 아무런 문제가 없어보인다. 그러나, 지금 저 문자열은 띄어쓰기가 제대로 되어 있지 않아서 SQL이 문제가 발생한다. 저 문자열을 풀어보면 이렇게 된다.
String sql = "select * from memberwhere name like ?and age between ? and ?"
- 이건 문법 오류를 발생시킬 것이다. 그런데 더 큰 문제는 문자열이라서 컴파일 시에 문제를 뱉어내지 않는다. 그래서 개발자는 자칫 실수를 할 수 있다. 이 문제가 위 JPA에서 동적 쿼리를 만들어낼때도 동일하게 적용된다. 조건에 따라 쿼리에 문자열을 추가하고 있지 않은가?
그리고, SQL을 작성할 때 과연 컬럼명을 다 외우면서 작성할 수 있을까? 물론 외울 수도 있다. 그런데 외울 필요없이 그냥 자바 코드 작성하듯 (.) 찍으면 사용 가능한 필드가 보여지고 그런 편의성을 제공해주면 얼마나 좋을까? 이를 위해 QueryDsl이 만들어졌다.
그래서 QueryDsl은 이런 장점이 있다.
- 동적 쿼리를 작성할 때 매우매우 막강하다.
- 타입 세이프한 쿼리를 작성하기에 좋다.
왜 이런 장점이 있는지 하나씩 알아보자.
QueryDsl 설정
일단, 가장 단점이라고 하면 QueryDsl은 설정이 조금 귀찮다. 그러나 이 설정의 귀찮음을 몇배는 더 능가하는 막강한 기능을 제공하기 때문에 안 쓸 이유가 없다. 한번 사용하면 못 돌아온다. 설정 관련 포스팅이 이미 있다.
Spring Boot 3.1.5에서 QueryDSL 설치하기
Spring Data JPA와 같이 사용하면 막강의 쿼리 작성을 할 수 있는 QueryDSL을 프로젝트에 설정하는 방법을 기록하고자 한다. 하도 버전에 따라 설치하는 방법이 달라져서 스프링 부트 3.1.5, Gradle에서 설
cwchoiit.tistory.com
이 포스팅을 보고 설정을 진행하자.
Q 파일
위 포스팅을 따라 진행을 하면, Q파일이 생긴것을 확인할 수 있을 것이다. 그런데 이 파일 어떻게 만드는건지 궁금하다면 이 포스팅을 참고해보면 좋다.
Lombok은 어떻게 동작하는걸까?
자바로 개발을 할때 이 지루한 코드들을 보거나 작성해 본 경험이 있으신가요?package cwchoiit;public class Member { private String name; private int age; public String getName() { return name; } public void setName(String name) {
cwchoiit.tistory.com
위 내용을 보면 애노테이션 프로세싱이라는 기술을 설명하는데 그 기술을 사용해서 컴파일 시점에 Q클래스들을 만들어내는 것이다. 그리고 그 클래스들을 만들어내는 타겟 애노테이션은 @Entity 애노테이션이다.
QueryDsl 적용
이제 QueryDsl을 적용해보자. 정말 사용해보면 굉장히 깔끔하고 딱 봐도 어떤 쿼리를 내보낼지가 눈에 보일것이다.
package hello.itemservice.repository.jpa;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import hello.itemservice.domain.Item;
import hello.itemservice.domain.QItem;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.ItemSearchCond;
import hello.itemservice.repository.ItemUpdateDto;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
import static hello.itemservice.domain.QItem.*;
@Repository
@Transactional
public class JpaItemRepositoryV3 implements ItemRepository {
private final EntityManager em;
private final JPAQueryFactory queryFactory;
public JpaItemRepositoryV3(EntityManager em) {
this.em = em;
this.queryFactory = 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);
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
return queryFactory
.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);
}
return null;
}
}
- 먼저 QueryDsl을 사용하기 위해서는 아래와 같이 JPAQueryFactory를 주입받아야 한다.
private final EntityManager em;
private final JPAQueryFactory queryFactory;
public JpaItemRepositoryV3(EntityManager em) {
this.em = em;
this.queryFactory = new JPAQueryFactory(em);
}
- 이 부분은 스프링 빈으로 등록해서 주입받을 수도 있고 이렇게 직접 주입 받아도 무방하다.
@Override
public List<Item> findAll(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
return queryFactory
.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);
}
return null;
}
- 이 부분이 QueryDsl을 사용한 부분이다. 일단 findAll()의 반환값을 보자. 누가봐도 어떤 쿼리가 나갈지가 너무 명확히 보인다.
- 그리고 동적 쿼리도 기가 막히게 해결한다. itemName 조건과 maxPrice 조건을 처리하는 것을 메서드로 빼버렸다. 그리고 QueryDsl에서 제공하는 BooleanExpression 타입을 받는데 이게 null일 경우 무시해버린다.
- 저렇게 where 조건 안에 (,)로 연결하면 AND로 이어준다.
- 또 강력한 장점 중 하나는 메서드로 조건을 빼버렸기 때문에 재사용이 가능하다는 점이다!
이렇게 깔끔하게 동적쿼리를 해결할 수 있었다. 참고로, QueryDsl은 스프링 데이터 접근 예외 추상화를 지원하지는 않는다. 그래서 이를 해결하기 위해 @Repository 애노테이션을 붙여서 스프링이 프록시로 변환시켜 만들어주는 해결 방식이 있다.
'Querydsl' 카테고리의 다른 글
참고: 리포지토리 지원 - QuerydslRepositorySupport (0) | 2024.12.30 |
---|---|
JPA 레포지토리와 Querydsl (2) | 2024.12.26 |
Querydsl 중급 문법 (0) | 2024.12.22 |
Querydsl 기본 문법 (0) | 2024.12.21 |
Spring Boot 3.1.5에서 QueryDSL 설치하기 (0) | 2023.11.26 |