728x90
반응형
SMALL

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 애노테이션을 붙여서 스프링이 프록시로 변환시켜 만들어주는 해결 방식이 있다.

 

 

 

728x90
반응형
LIST

'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

+ Recent posts