스프링 부트 - 주문 검색 기능 개발 학습내용
"실전! - 스프링부트와 JPA 활용1 강의를 듣고 작성한 글 입니다."
실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 인프런
실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다. 초급
www.inflearn.com
검색 기능을 개발함에 있어 핵심적인 내용?
- JPA 에서의 동적 쿼리는 어떻게 해결하는가?
이번 예제 프로젝트에 서 주문 검색을 할 경우, 회원 명과 주문 상태를 이용할 수 있도록 되어 있다.
이를 통해 알 수 있는 것은 결국 동적 쿼리가 만들어져야 하는 상황인 것이다.
파라미터로 회원 명을 받으면 where 문에 회원명이 들어가야 하고, 주문 상태가 선택되면 where 문에 주문 상태가 들어가야 한다.
(보통 실무에서는 동적 쿼리를 안 쓸 수가 없다.)
Repository 에서 OrderSearch 클래스의 데이터를 받을 것이므로, 같은 패키지 내부에 OrderSearch 클래스를 만들어준다.
- OrderSearch.java
@Getter
@Setter
public class OrderSearch {
private String memberName;
private OrderStatus orderStatus;
}
위와 같이 클래스를 만들어 줬으면 이제 Repository 클래스에서 JPQL 을 이용해 동적 쿼리를 만들어야 한다.
* 여기서 중요한 점?
주문 검색을 하는데 있어 파라미터 값으로 회원 명과 주문 상태를 받는데, 두 데이터가 모두 필수로 입력된다고 가정하면 파라미터 바인딩을 통해 간편하게 JPQL 을 작성하면 되겠지만, 두 데이터 중 하나만 입력해도 검색 결과가 잘 나오게끔 하려고 한다면 이야기가 복잡해진다.
-> 회원 명 데이터의 입력이 존재하지 않으면, 해당 데이터는 사용하지 말고 주문 내역을 모두 가져와야 한다.
(회원 명 상관없이 입력받은 주문 상태의 주문 내역을 모두 검색)
-> 주문 상태 데이터의 입력이 존재하지 않으면, 해당 데이터는 사용하지 말고 주문 내역을 모두 가져와야 한다.
(주문 상태 상관없이 입력받은 회원 명의 주문 내역을 모두 검색)
- OrderRepository.java
public List<Order> findAll(OrderSearch orderSearch) {
return em.createQuery("select o from Order o join o.member m", Order.class)
.setMaxResults(1000) // 최대 1000건
.getResultList();
}
위와 같은 상태에서 동적 쿼리를 어떻게 작성해 줄 수 있을까?
(데이터를 동적으로 할당 받아야 하기 때문에 setParameter 를 이용한 파라미터 바인딩은 사용할 수 없다.)
1. 첫번째, 무식한 방법
JPQL 그대로 해결한다.
- OrderRepository.java
public List<Order> findAll(OrderSearch orderSearch) {
String jpql = "select o from Order o join o.member m";
boolean isFirstCondition = true;
// 주문 상태 검색
if(orderSearch.getOrderStatus() != null) { // 주문 상태 입력이 있을 경우(회원 명X)
if(isFirstCondition) { // 데이터가 조건절의 첫번째로 들어온 경우
jpql += "where";
isFirstCondition = false;
} else { // 조건절의 첫번째 요소가 아닌 경우
jpql += " and";
}
jpql += " o.status = :status";
}
if(StringUtils.hasText(orderSearch.getMemberName())) { // 회원 명 데이터 입력이 있을 경우(주문 상태X)
if(isFirstCondition) {
jpql += " where";
isFirstCondition = false;
} else {
jpql += " and";
}
jpql += " m.name like :name";
}
TypedQuery<Order> query = em.createQuery(jpql, Order.class)
.setMaxResult(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();
}
이런식으로 문자열 조작을 통해 무식하게 동적 할당을 하는 방법이 있으나 굉장히 복잡하고 귀찮다.
그리고 사실상 이렇게 문자열을 더해가며 동적 할당을 하는 것은 정말 힘들뿐더러, 실무 같은 복잡한 상황에서 하기엔 거의 불가능에 가깝다. (버그도 찾기 힘들다.)
* 여기서 나오는 MyBatis 를 쓰는 이유? : 동적 쿼리를 작성하는데 있어 이점이 있기 때문
2. 두번째 방법
위의 첫번째 방법보다 조금 더 나은 방법이다.
JPA Criteria : JPQL 을 자바 코드로 작성할 수 있게끔 JPA 에서 표준으로 제공해주는 것
이것도 권장하는 방법은 아님(사실상 실무에서 쓸 수 있는 방법이 아니다.)
- OrderRepository.java
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();
}
이 방법이 가진 가장 치명적인 단점?
도대체 어떤 JPQL 코드가 생성될지 감이 잡히지 않는다.
JPA 표준 스펙이지만 실무에서 사용하기에 너무 복잡하다.
결국 다른 대안이 필요하다.
(JPA Criteria 에 대한 자세한 내용은 자바 ORM 표준 JPA 프로그래밍 책을 참고하자.)
3. 세번째 방법
QueryDSL 로 동적쿼리 처리하기
QueryDSL 은 동적 쿼리에 대한 강력한 해결책이다.
- 자바 코드로 작성되기 때문에 오타와 같은 오류가 발생하면 컴파일 시점에 전부 오류를 잡아준다.
- QueryDSL 의 경우는 추후에 따로 강의에서 설명한다.