주문 도메인 개발

유병익·2022년 11월 29일
0
post-thumbnail

1. Plan


  • 구현 기능
    • 상품 주문
    • 주문 내역 조회
    • 주문 취소
  • 순서
    • Order, OrderItem Entity 개발
    • Repository 개발
    • Service 개발
    • 주문 검색 기능 개발
    • 기능 테스트



2. Order, OrderItem Entity 개발


2.1 Order Entity


  • Order.java
    package jpabook.jpashop.domain;
    
    import lombok.Getter;
    import lombok.Setter;
    
    import javax.persistence.*;
    import java.time.LocalDateTime;
    import java.util.ArrayList;
    import java.util.List;
    
    @Entity
    @Table(name = "orders")
    @Getter
    @Setter
    public class Order {
    
        @Id
        @GeneratedValue
        @Column(name = "order_id")
        private Long id;
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "member_id")
        private Member member;
    
        @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
        private List<OrderItem> orderItems = new ArrayList<>();
    
        @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
        @JoinColumn(name = "delivery_id")
        private Delivery delivery;
    
        private LocalDateTime orderDate; // 주문 시간
    
        @Enumerated(EnumType.STRING)
        private OrderStatus status; // 주문 상태 [ORDER, CANCEL]
    
        /*
            #연관 관계 편의 메소드
            양방향 관계에서 한번에 양쪽 다 세팅
         */
        public void setMember(Member member) {
            this.member = member;
            member.getOrders().add(this);
        }
        public void addOrderItem(OrderItem orderItem) {
            this.orderItems.add(orderItem);
            orderItem.setOrder(this);
        }
    
        public void setDelivery(Delivery delivery) {
            this.delivery = delivery;
            delivery.setOrder(this);
        }
    
        // 생성 메서드
        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 : orderItems) {
                orderItem.cancel();
            }
        }
    
        //조회 로직
        /*
        전체 주문 가격 조회
         */
        public int getTotalPrice() {
            int totalPrice = 0;
            for (OrderItem orderItem : orderItems) {
                totalPrice += orderItem.getTotalPrice();
            }
            return totalPrice;
        }
    }

기능 설명

  • createOrder()
    • 생성 메서드
    • 주문 엔티티를 생성할 때 사용
    • 주문 회원, 배송정보, 주문상품의 정보를 받아 실제 주문 엔티티 생성
  • cancel()
    • 주문 취소시 사용
    • 주문 상태를 취소로 변경
      • 주문상품에 주문 취소 처리
    • 이미 배송을 완료한 상품이면 주문을 취소하지 못하도록 예외 발생
  • getTotalPrice()
    • 주문 시 사용한 전체 주문 가격 조회



2.2 Order Item Entity


  • OrderItem.java
    package jpabook.jpashop.domain;
    
    import jpabook.jpashop.domain.item.Item;
    import lombok.Getter;
    import lombok.Setter;
    
    import javax.persistence.*;
    
    @Entity
    @Getter
    @Setter
    public class OrderItem {
    
        @Id
        @GeneratedValue
        @Column(name = "order_item_id")
        private Long id;
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "item_id")
        private Item item;
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "order_id")
        private Order order;
    
        private int orderPrice;
        private int count;
    
        //생성 메서드
        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 getCount() * getOrderPrice();
        }
    }

기능 설명

  • createOrderItem()
    • 주문 상품, 가격, 수량 정보를 사용해서 주문상품 엔티티를 생성
    • item.removeStock(count) 호출
      • 주문한 수량만큼 상품의 재고 감소
  • cancel()
    • getItem().addStock(count) 호출
      • 취소한 주문 수량만큼 상품의 재고를 증가
  • getTotalPrice()
    • 주문 가격에 수량을 곱한 값을 반환



3. Order Repository 개발


  • OrderRepository.java
    package jpabook.jpashop.repository;
    
    import jpabook.jpashop.domain.Member;
    import jpabook.jpashop.domain.Order;
    import lombok.RequiredArgsConstructor;
    import org.springframework.stereotype.Repository;
    import org.springframework.util.StringUtils;
    
    import javax.persistence.EntityManager;
    import javax.persistence.TypedQuery;
    import javax.persistence.criteria.*;
    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 orderId) {
            return em.find(Order.class, orderId);
        }
    }



4. Order Service 개발


  • OrderService.java
    package jpabook.jpashop.service;
    
    import jpabook.jpashop.domain.Delivery;
    import jpabook.jpashop.domain.Member;
    import jpabook.jpashop.domain.Order;
    import jpabook.jpashop.domain.OrderItem;
    import jpabook.jpashop.domain.item.Item;
    import jpabook.jpashop.repository.ItemRepository;
    import jpabook.jpashop.repository.MemberRepository;
    import jpabook.jpashop.repository.OrderRepository;
    import jpabook.jpashop.repository.OrderSearch;
    import lombok.RequiredArgsConstructor;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    import java.util.List;
    
    @Service
    @RequiredArgsConstructor
    @Transactional(readOnly = true)
    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();
        }
    
        //주문 취소
        @Transactional
        public void cancelOrder(Long orderId) {
            //주문 엔티티 조회
            Order order = orderRepository.findOne(orderId);
            //주문 취소
            order.cancel();
        }
    }
  • order()
    • 주문하는 회원 식별자, 상품 식별자, 주문 수량 정보로 실제 주문 엔티티를 생성 및 저장
  • cancelOrder()
    • 주문 식별자를 받아 주문 엔티티를 조회한 후 주문 엔티티에 주문 취소를 요청



5. 주문 기능 테스트


  • OrderServiceTest.java
    ```java
    package jpabook.jpashop.service;
    
    import jpabook.jpashop.domain.Address;
    import jpabook.jpashop.domain.Member;
    import jpabook.jpashop.domain.Order;
    import jpabook.jpashop.domain.OrderStatus;
    import jpabook.jpashop.domain.item.Book;
    import jpabook.jpashop.domain.item.Item;
    import jpabook.jpashop.exception.NotEnoughStockException;
    import jpabook.jpashop.repository.OrderRepository;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
    import org.springframework.transaction.annotation.Transactional;
    
    import javax.persistence.EntityManager;
    
    import static org.junit.Assert.assertEquals;
    import static org.junit.Assert.fail;
    
    @RunWith(SpringRunner.class)
    @Transactional
    @SpringBootTest
    public class OrderServiceTest {
    
        @Autowired
        EntityManager em;
        @Autowired
        OrderService orderService;
    
        @Autowired
        OrderRepository orderRepository;
    
        @Test
        public void 상품주문() throws Exception {
            //given
            Member member = createMember();
    
            Book book = createBook("책이름1", 10000, 100);
    
            int orderCount = 3;
            //when
            Long orderId = orderService.order(member.getId(), book.getId(), orderCount);
    
            //then
            Order findOrder = orderRepository.findOne(orderId);
            assertEquals("상품 주문 시 상태는 ORDER여야 함", OrderStatus.ORDER, findOrder.getStatus());
            assertEquals("주문항 상품 종류의 수가 일치해야한다.", 1, findOrder.getOrderItems().size());
            assertEquals("주문 가격은 가격 * 수량 이다.", 30000, findOrder.getTotalPrice());
            assertEquals("주문 수량 만큼 재고가 줄어야 한다.", 97, book.getStockQuantity());
        }
    
        @Test(expected = NotEnoughStockException.class)
        public void 상품주문_재고수량초과() throws Exception {
            //given
            Member member = createMember();
            Item item = createBook("책이름1", 10000, 100);
            int orderCount = 101;
    
            //when
            orderService.order(member.getId(), item.getId(), orderCount);
    
            //then
            fail("재고 수량 예외가 발생해야 한다.");
        }
    
        @Test
        public void 주문취소() throws Exception {
            //given
            Member member = createMember();
            Item item = createBook("책이름1", 10000, 100);
    
            int orderCount = 30;
            Long orderId = orderService.order(member.getId(), item.getId(), orderCount);
    
            //when
            orderService.cancelOrder(orderId);
    
            //then
            Order findOrder = orderRepository.findOne(orderId);
            assertEquals("주문 취소 시 상태는 CANCEL이다.", OrderStatus.CANCEL, findOrder.getStatus());
            assertEquals("주문이 취소되면 재고가 원상복구 되어야한다.", 100, item.getStockQuantity());
        }
    
        private Member createMember() {
            Member member = new Member();
            member.setName("회원1");
            member.setAddress(new Address("서울", "길거리", "15243"));
            em.persist(member);
            return member;
        }
    
        private Book createBook(String name, int price, int stockQuantity) {
            Book book = new Book();
            book.setName(name);
            book.setPrice(price);
            book.setStockQuantity(stockQuantity);
            em.persist(book);
            return book;
        }
    }
    ```
    
    > **Given**
    > 
    > - 테스트를 위한 회원과 상품을 생성
    > 
    > **When**
    > 
    > - 실제 상품을 주문
    > 
    > **Then**
    > 
    > - 주문 가격이 올바른지, 주문 후 재고 수량이 정확히 줄었는지 검증


6. 주문 검색 기능 개발


  • OrderSearch.java
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; // 주문 상태
}
  • OrderRepository에 추가
public List<Order> findAll(OrderSearch orderSearch) {
 //... 검색 로직
 }
📌 **findAll(OrderSearch orderSearch)**
  • 검색 조건에 동적으로 쿼리를 생성해서 주문 엔티티를 조회한다.
  • 동적인 쿼리를 만드는 방법은 크게 3가지로 나뉜다.
    - JPQL을 직접 생성해서 처리
    - JPA Criteria
    - Query DSL



6.1 JPQL을 직접 생성해서 처리


  • Code
    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();
    }
📌 JPQL 쿼리를 문자로 생성하기는 번거롭고, 실수로 인한 버그가 발생하기 쉬움



6.2 JPA Criteria


  • Code
    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();
        }
  • JPA Criteria는 JPA 표준 스펙
    • 하지만 실무에서 사용하기에 너무 복잡하다.
  • Querydsl을 사용하자!
profile
Backend 개발자가 되고 싶은

0개의 댓글