JPA를 사용하여 쿼리를 작성하다보면 동적 쿼리의 필요성에 대해 느낄 때가 있다. 다음 예시를 통해 정적쿼리에서 동적쿼리가 필요할 때 어떤 방식들이 있었는지 정리해보았다.
정적 쿼리를 사용하는 상황
예를들어 이름과 상태 값에 따른 결과 검색을 해야하는 상황이 있다고 생각해보자.
JPA를 사용하게 되면 JPQL 쿼리를 입력하고 이름과 상태 파라미터에 맞는 값들을 바인딩해주어 결과를 받는 코드를 작성할 수 있다.
em.createQuery("select o from Order o join o.member m" +
" where o.status = :status" +
" and m.name like :name", Order.class)
.setParameter("status", orderSearch.getOrderStatus())
.setParameter("name", orderSearch.getMemberName())
.setMaxResults(1000) //최대 1000건
.getResultList();
하지만 위와 같은 방식을 사용하게 되면 status와 name을 모두 사용하여야 한다. 만약 이름은 알지만 상태를 모르는 경우나 상태는 알지만 이름은 정확히 모르는 경우에는 검색이 어려울 것이다.
그렇다고 단순하게 총 4가지(변수가 2개이므로) 상황에 대해서 모든 메서드를 작성하는 것은 비효율적이고 칸을 많이 차지하기 때문에 개발자로서 코드를 개선하고 싶어 근질거릴 것이다. 2개이기에 망정이지 10가지의 경우라면? 생각만해도 끔찍하다.
이렇게 정적쿼리만을 사용하게 된다면 이런 문제를 해결할 수 없다.
따라서 지금 필요한 것은 검색할 때 들어오는 파라미터에 상관없이 동적 쿼리를 짤 수 있도록 만드는 것이다.
동적 쿼리1 - 직접 분기하여 JPQL 쿼리문 작성
가장 쉬운 방법으로는 들어오는 파라미터의 상태에 따라 직접 분기를 하고 수동으로 쿼리를 조립하는 방법이다.
Order에 대한 쿼리는 "select o From Order o join o.member m" 이와 같을 것이고 이후에 where문의 조건에 들어갈 파라미터를 식별한 후에 알맞은 쿼리를 짜야할 것이다. 이 후 query를 만들주고 파라미터에 따른 바인딩을 각각 해주는 방식 후 마지막으로 getResultList()를 사용하여 결과를 받아 볼 수 있다.
String jpql = "select o From Order o join o.member m"
||
\/
//주문 상태 검색 분기
if 상태값이 존재하면
첫 조건인지에 대한 분기
jpql += " where";
jpql += " and";
jpql += " o.status = :status";
//회원 이름 검색
if 이름값이 존재하면
첫 조건인지에 대한 분기
jpql += " where";
jpql += " and";
jpql += " m.name like :name";
이를 기반으로 쿼리를 만들고,
TypedQuery<Order> query = em.createQuery(jpql, Order.class) .setMaxResults(1000); //최대 1000건
||
\/
각 파라미터에 맞는 바인딩
//주문 상태 검색 바인딩
if 상태값이 존재하면
query = query.setParameter("status", orderSearch.getOrderStatus());
//회원 이름 바인딩
if 이름값이 존재하면
query = query.setParameter("name", orderSearch.getMemberName());
||
\/
마지막으로 결과 받기
return query.getResultList();
단순히 보기만해도 비즈니스 로직에 따라 복잡해질 수 있고 많은 bug가 생길 수 있는 문제점을 가지고 있다.
동적 쿼리2 - JPA Criteria 사용하여 동적 쿼리
다음 방법으로는 JPA 표준 스펙인 JPA Criteria를 사용하여 작성하는 방법이다. 위와 개념은 비슷하지만 JPA에서 제공하는 인터페이스와 클래스를 사용해 조건을 달아 작성하는 방법이다.
public List<Order> findAllByCriteria(OrderSearch orderSearch) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Order> cq = cb.createQuery(Order.class);
Root<Order> o = cq.from(Order.class);
Join<Object, Object> m = o.join("member", JoinType.INNER);
List<Predicate> criteria = new ArrayList<>();
//주문 상태 검색
if (orderSearch.getOrderStatus() != null) {
Predicate status = cb.equal(o.get("status"), orderSearch.getOrderStatus());
criteria.add(status);
}
//회원 이름 검색
if (StringUtils.hasText(orderSearch.getMemberName())) {
Predicate name =
cb.like(m.<String>get("name"), "%" +
orderSearch.getMemberName() + "%");
criteria.add(name);
}
cq.where(cb.and(criteria.toArray(new Predicate[criteria.size()])));
TypedQuery<Order> query = em.createQuery(cq).setMaxResults(1000); //최대 1000건
return query.getResultList();
}
하지만 보는바와 같이 너무 복잡하고 작성해야하는 쿼리가 쉽게 떠오르지 않아 유지보수가 힘들기 때문에 실무에서 사용하기 힘든 단점을 지니고 있다.
동적 쿼리3 - QueryDSL
좀 더 fancy한 방법으로는 QueryDSL을 사용하는 방법이 있다. QueryDSL은 추가적으로 gradle에 의존성을 추가해주어야 사용 가능하며 여럿 설정을 해주어야 한다.
/*
* QueryDSL
*/
public List<Order> findAll(OrderSearch orderSearch) {
QOrder order = QOrder.order;
QMember member = QMember.member;
return query
.select(order)
.from(order)
.join(order.member, member)
.where(statusEq(orderSearch.getOrderStatus()),
nameLike(orderSearch.getMemberName()))
.limit(1000)
.fetch();
}
실무에서는 앞서 설명한 2가지 방법보다 가독성이 높고 실수할 가능성이 적기 때문에 QueryDSL과 함께 JPA를 사용하여 코드 개발에 있어 생산성을 높인다고 한다.
지금까지 정적쿼리에서 동적쿼리로 만들기 위해서 어떤 방식들이 존재했는지 알아보았다. JPA를 공부하고 동적 쿼리를 사용해야할 때 본격적으로 QueryDSL에 대해서도 공부를 더 해보면 좋을 것 같다.
QueryDSL에 대한 추가적인 설명은 다음 글을 참조해보면 좋을 것 같다.
[Spring] QueryDSL로 조건검색 API를 만들어보자(동적 쿼리)
개발을 하다보면 위의 그림 처럼 여러가지 조건을 가지고 검색을 해야할 경우가 많이 있습니다. 조건이 고정되어 있다면 조금 쉽겠지만 사용자가 필요에 따라 조
velog.io
'Web Dev > Spring' 카테고리의 다른 글
[Spring+JPA] 간단 노트 정리 (0) | 2023.03.17 |
---|---|
[JPA] 도메인 설계 정리 (0) | 2023.03.14 |
[Spring] 스프링 빈 충돌 이슈 관리 (0) | 2023.03.12 |
Spring - Paging 처리 (0) | 2022.08.22 |
Spring Security - Filter의 동작 원리 (0) | 2022.08.02 |