Backend

[실전! 스프링부트와 JPA활용] 주문검색 기능개발 : JPQL, JPA Criteria, Querydsl

햣둘 2025. 4. 9. 07:55

JPA에서 동적 쿼리를 어떻게 해결해야 하는가?

 

검색 조건 파라미터 OrderSearch

package jpabook.jpashop.repository;

import jpabook.jpashop.domain.OrderStatus;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class OrderSearch {

    private String memberName; // 회원이름
    private OrderStatus orderStatus; // 주문상태 (ORDER, CANCEL)

}

 

검색을 추가한 주문 레포지토리 코드

findAll(OrderSearch orderSearch) 메서드는 검색 조건에 동적으로 쿼리를 생성해서 주문 엔티티를 조회한다.

package jpabook.jpashop.repository;

import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.*;
import jpabook.jpashop.domain.Order;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.List;

@Repository
@RequiredArgsConstructor
public class OrderRepository {

    private final EntityManager em;

    public void save(Order order) {
        em.persist(order);
    }

    public Order findOne(Long id) {
        return em.find(Order.class, id);
    }

    public List<Order> findAllByString(OrderSearch orderSearch) {
	// ... 검색 로직
    }
}

 

JPQL로 처리

JPQL 쿼리를 문자로 생성하기는 번거롭고, 실수로 인한 버그가 충분히 발생할 수 있다.

/**
 * JPQL로 처리
 * 실무에선 이렇게 안 씀
 * 하나하나 다 일일이 짜는 느낌
 */
public List<Order> findAllByString(OrderSearch orderSearch) {

    // language=JPQL
    String jpql = "select o from Order o join o.member m";
    boolean isFirstCondition = true;

    // 주문 상태 검색
    if (orderSearch.getOrderStatus() != null) {
        if (isFirstCondition) {
            jpql += "where";
            isFirstCondition = false;
        } else {
            jpql += "and";
        }
        jpql += "o.status = :status";
    }

    // 회원 이름 검색
    if (StringUtils.hasText(orderSearch.getMemberName())) {
        if (isFirstCondition) {
            jpql += "where";
            isFirstCondition = false;
        } else {
            jpql += " and";
        }
        jpql += "m.name like :name";
    }

    TypedQuery<Order> query = em.createQuery(jpql, Order.class)
            .setMaxResults(1000);  // 최대 1000건
    if (orderSearch.getOrderStatus() != null) {
        query = query.setParameter("status", orderSearch.getOrderStatus());
    }
    if (StringUtils.hasText(orderSearch.getMemberName())){
        query = query.setParameter("name", orderSearch.getMemberName());
    }

    return query.getResultList();
}

 

JPA Criteria로 처리

/**
     * JPA Criteria 표준스펙에 있는 내용이지만 실무에선 안 씀
     * 이렇게 짜면 무슨 jpql을 만드는지 감이 안 옴
     * 유지보수를 거의 못하게 짠 코드임
     */
    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);
        return query.getResultList();
    }
}

 

JPA Criteria는 JPA 표준 스펙이지만, 실무에서 사용하기에 너무 복잡하다.

결국 다른 대안이 필요하다.

많은 개발자가 비슷한 고민을 했지만, 가장 멋진 해결책은 Querydsl이 제시했다.

Querydsl은 강의영상 소개장에서 간단히 언급한다.

지금은 이대로 진행하자.

 

참고) JPA Criteria에 대한 자세한 내용은 자바 ORM 표준 JPA프로그래밍 책을 참고하자.