프로그래밍/Spring

[Spring boot / JPA] 6. 주문 도메인 개발

daykim 2023. 7. 3. 23:33
아래 강의 정리
 

실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 인프런 | 강의

실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다., 스프

www.inflearn.com

목차

  • 주문, 주문 상품 엔티티 개발
  • 주문 서비스 개발
  • 주문 기능 테스트
  • 주문 검색 기능 개발

 

구현 기능

  • 상품 주문
  • 주문 내역 조회
  • 주문 취소

 

주문, 주문 상품 엔티티 개발


  • createOrder (생성 메서드)
    • 주문 엔티티를 생성할 때 사용한다.
    • 주문 회원, 배송 정보, 주문 상품의 정보를 받아서 실제 주문 엔티티를 생성한다.
  • cancle (주문 취소)
    • 주문 취소시 사용한다.
    • 문 상태를 취소로 변경하고, 주문 상품에 주문 취소를 알린다.
    • 만약 이미 배송을 완료한 상품이면, 주문을 취소하지 못 하도록 예외를 발생시킨다.
  • 전체 주문 가격 조회
    • 주문시 사용한 전체 주문 가격을 조회한다.
    • 전체 주문 가격을 알려면, 각각의 주문상품 가격을 알아야 한다.
    • 로직을 보면, 연관된 주문상품들의 가격을 조회해서 더한 값을 반환한다.
    • 실무에서 주로 주문에 전체 주문 가격 필드를 두고 역정규화 한다.

 

 

주문 서비스 개발


  • 주문 엔티티와 주문 상품 엔티티의 비즈니스 로직을 활용해 주문, 주문 취소, 주문 내역 검색 기능을 제공한다.
  • 5:30 -> casecade 범위를 어디까지 해야할까
  • OrderItem() 생성자 : 생성 로직을 정해둔 방법으로만 하도록, 다른 스타일 생성을 다 막는 것이다.
    • JPA는 protected까지 기본 생성자 만드는 것을 허용해준다.
    • 아래 코드로 해결할 수 있다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
  • JPA를 활용하면, 엔티티 안의 데이터만 바꾸면 JPA가 알아서 바뀐 변경 포인트를, 더티 체킹?(변경된 내역 감지)가 일어나며, 변경 내역 찾아서 데이터베이스에 업데이트 쿼리가 알아서 날아간다. -> JPA의 엄청 큰 장

 

참고
주문 서비스의 주문과 주문 취소 메서드를 보면 비즈니스 로직 대부분이 엔티티에 있다.
서비스 계층은 단순히 엔티티에 필요한 요청을 위임하는 역할을 한다.
이처럼 엔티티가 비즈니스 로직을 가지고 객체 지향의 특성을 적극 활용하는 것을 도메인 모델 패턴이라 한다.
반대로 엔티티에는 비즈니스 로직이 거의 없고 서비스 계층에서 대부분 의 비즈니스 로직을 처리하는 것을 트랜잭션 스크립트 패턴이라 한다.

 

 

주문 기능 테스트


테스트 요구사항

  • 상품 주문이 성공해야 한다.
  • 상품을 주문할 때 재고 수량을 초과하면 안 된다.
  • 주문 취소가 성공해야 한다.

 

 

 

주문 검색 기능 개발


JPA 동적 쿼리 개발

return em.createQuery("select o from Order o join o.member m" +
                " where o.status = :status" +
                " and m.name like :name", Order.class) //JPQL
                .setParameter("status", orderSearch.getOrderStatus())
                .setMaxResults(1000) // 최대 1000건
                .getResultList();
  • status나 name이 null인 경우를 어떻게 처리해야 하는지에 대한 것이다.

 

1. 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";
        }
        // 회원 이름 검색
        // ...
        
        TypeQuery<Order> query = em.createQuery(jpql, Order.class)
            .setmaxResults(1000);
            
       // ..
        
        return query.getResultList();
  • 다 작성하지 않았다.
  • JPQL 쿼리를 문자로 생성하기는 무척 번거롭고, 실수로 버그가 발생하기 쉽다.
  • 따라서 권장하진 않는다.
  • Mybatis를 사용하면 동적 쿼리 생성이 훨씬 쉽다고 한다.

 

2. findAllByCriteria

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);
}
// 회원 이름 검색
...
        
cq.where(cb.and(criteria.toArray(new Predicate[criteria.size()])));
TypedQuery<Order> query = em.createQuery(cq).setMaxResults(1000);
return query.getResultList();
  • JPA Criteria는 JPA 표준 스펙이다. 동적 쿼리를 빌드해주는..?
  • 요거도 실무에서 사용하기 너무 어려워 크게 권장하진 않는다.
  • Predicate 자체가 조건이 되는 것이다.
  • 실행시키면 JPQL이 만들어진다.
  • 코드를 봤을 때, 무슨 쿼리가 생성될지 떠오르지 않는다.
    => 유지보수성이 거의 없다.

 

3. Querydsl

  • 이에 대한 대안이 Querydsl이다.
  • 자세한건 나중에