[실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발] 주문 도메인 개발

이재표·2023년 10월 28일
0

주문 도메인을 통해 트랜잭션 스크립트 패턴과 도메인 모델패턴을 접해보도록 하자!

구현 기능

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

주문

상품 주문 기능, 주문 취소 기능, 주문 조회 기능을 만들어 보자!

@Entity
@Table(name = "orders")
@Getter
@Setter
public class Order {
...
    //==생성메서드==//
    public static Order createOrder(Member member,Delivery delivery,OrderItem... orderItems){
        Order order = new Order();
        order.setMember(member);
        order.setDelivery(delivery);
        for(OrderItem orderItem:orderItems){
            order.addOrderItem(orderItem);
        }
        order.setStatus(OrderStatus.ORDER);
        order.setOrderDate(LocalDateTime.now());
        return order;
    }

    //==주문 취소==//
    public void cancel(){
        if(delivery.getStatus()==DeliveryStatus.COMP){
            throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
        }
        this.setStatus(OrderStatus.CANCEL);
        for(OrderItem orderItem:orderItemList){
            orderItem.cancel();
        }
    }

    //==조회 로직==//
    /*
    전체 주문 가격 조회
     */
    public int getTotalPrice(){
        int totalPrice = 0;
        for (OrderItem orderItem : orderItemList) {
            totalPrice += orderItem.getTotalPrice();
        }
        return totalPrice;
    }
}

주문상품

주문상품 엔티티를 통해 주문된 상품의 정보를 생성하고, 주문된 상품들의 가격이나, 주문된 상품을 취소하는 메서드를 만들어보자

public class OrderItem {
...
    //==생성 메서드==//
    public static OrderItem createOrderItem(Item item,int orderPrice,int count){
        OrderItem orderItem = new OrderItem();
        orderItem.setItem(item);
        orderItem.setOrderPrice(orderPrice);
        orderItem.setCount(count);
        item.removeStock(count);
        return orderItem;
    }

    //==비즈니스 로직//
    public void cancel() {
    getItem().addStock(count);
    }

    //==조회메서드==//
    public int getTotalPrice() {
        return getOrderPrice() * getCount();
    }
}

Repository

엔티티 매니저를 통해 엔티티를 DB에 저장해보는데, 이때 스프링을 이용하면 EntityManagerFactory를 이용하는 것이 아닌 생성자 주입을 통해 EntityManager를 생성할수 있다.

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> findAll(OrderSearch orderSearch) { ... }
}

검색 기능의 경우 동적 쿼리가 들어가야하기 때문에 추후 설명

Service

엔티티와 리포지토리에서 로직과 저장을 해결하였기 때문에 서비스 레이어에서는 역할 위임만 하여 해당 메서드들을 실행시켜 로직을 완성해준다!

public class OrderService {
    private final OrderRepository orderRepository;
    private final MemberRepository memberRepository;
    private final ItemRepository itemRepository;
    
    /*
    주문
     */
    @Transactional
    public Long order(Long memberId,Long itemId,int count){
        //엔티티 조회
        Member member = memberRepository.findOne(memberId);
        Item item = itemRepository.findOne(itemId);
        
        //배송정보 생성
        Delivery delivery = new Delivery();
        delivery.setAddress(member.getAddress());
        
        //주문상품 생성
        OrderItem orderItem = OrderItem.createOrderItem(item, item.getPrice(), count);
        
        //주문 생성
        Order order = Order.createOrder(member, delivery, orderItem);
        
        //주문 저장
        orderRepository.save(order);
        return order.getId();
    }
}

위의 로직을 보면 사실 orderItem이 저장되고 order에 들어가고 하는게 맞는 순서이지만 cascade.ALL을 걸어 영속성 전이를 했기 때문에 order가 저장될때 orderItem도 함께 저장되게 된다.

이때 오더가 오더아이템을 관리하는데 다른것들이 해당을 오더아이템을 참조하지 않고 오더만이 관리할때 이런경우에만 영속성 전이를 사용해야한다. 참조되는 엔티티가 많은경우 영속성 전이를 사용하지 않고 따로 리포지토리로 저장해야한다.

만약 다른사람들과 협업할경우 나는 createOrder를 통해 생성 메서드가 있다는 것을 알지만 다른사람들은 모르기에 생성자를 만들어 해당 생성자에 값을 넣는 형식으로 진행할수도 있다. 이것을 막기 위해 생성자를 protected로 규제해줘야 한다.
이때 어노테이션으로 @NoArgsConstructor(access = AccessLevel.PROTECTED)와 같이 사용할수 있다.

orderService에 취소 로직을 추가할때 값이 변경되는 경우가 많은데 어떠한 sql쿼리도 없다.

@Transactional
    public void cancelOrder(Long orderId){
        //주문 엔티티 생성
        Order order = orderRepository.findOne(orderId);
        //주문 취소
        order.cancel();
    }

jpa를 쓰면 더티체킹을 하기 때문에 따로 sql쿼리를 작성하지 않더라도 jpa가 쿼리를 만들어 던져준다.

검색 기능

모든 파라미터가 주어져 있다면 이렇게 where절로 쓰면 되겠지만 그렇지 않다면 동적쿼리로 파아미터가 null이면 다 가져오고,아니면 특정 값만 가져오고 이런식으로 구현해야한다.

 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())
                .setMaxResults(1000)
                .getResultList();

jpql문을 직접 조작하는 방법

다음과 같이 직접 jpql의 쿼리문을 조건에 맞게 조작한다. 너무 불편하고 문자 하나로 버그가 나기도 함..

public List<Order> findAllByString(OrderSearch orderSearch) {
	//language=JPAQL
    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

해당 방법도 지향하는 방법은 아니다. 왜냐하면 쿼리를 보는것이 너무 불편하기 때문에 유지보수가 어렵다. 가장 좋은 방법은 Querydsl을 사용하는 방식이다.

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<Order, Member> 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();
}

추후 querydsl을 설명할테니 현재는 criteria를 사용하자!!

0개의 댓글