주문 도메인 개발

OneTwoThree·2023년 8월 9일
0

출처


엔티티 개발

@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")
    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;

    // 연관관계 메서드
    public void setMember(Member member){
        this.member = member;
        member.getOrders().add(this);
    }

    public void addOrderItem(OrderItem orderItem){
        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에서 가변 인자 문법을 사용해 OrderItem 여러개를 받을 수 있도록 했다.
Order 생성은 복잡하기 때문의 별도의 생성 메서드를 활용해 연관관계를 모두 걸고 상태까지 결정해서 생성할 수 있다.

리포지토리 개발

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

}

서비스 개발

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
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();
    }

    /**
     *  검색
     */
//    public List<Order> findOrders(OrderSearch orderSearch){
//        return orderRepository.findAll(orderSearch);
//    }

}

orderRepository.save(order) 만 해도 다른 객체들이 같이 저장된다. 왜냐하면 CASCADETYPE=ALL로 지정했기 때문이다. 위 예시처럼 Order에서 다른 객체를 관리하는 경우는 이렇게 사용해도 된다. Order에서만 관리해야 한다. 다른 객체에서도 OrderItem을 관리한다면 이렇게 사용하면 안된다. (라이프사이클이 일치할 때)

createOrderItem 같은 생성 메소드를 사용하면 좋은 점은 유지보수하기 편하다는 것이다. 따라서 이렇게 생성하는 방식 외에 다른 생성 방식을 막아야 한다. 생성자를 protected로 만들면 된다.
lombok을 활용할 수도 있다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)를 사용해서 외부에서 생성자에 접근할 수 없게 막아준다.
이렇게 코드를 제약하는 스타일로 작성해야 좋은 설계와 유지보수가 가능하다.

비즈니스 로직이 대부분 엔티티에 있고 서비스는 엔티티에 처리를 위임하는 역할만 한다. 이것을 도메인 모델 패턴이라 한다. 엔티티에는 비즈니스 로직이 거의 없고 서비스 계층에서 비즈니스 로직을 처리하는 것을 트랜잭션 스크립트 패턴이라 한다.

어떤 방식이 더 유지보수하기 쉬운 지 고민해보고 선택하면 된다.

테스트

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class OrderServiceTest {

    @Autowired
    EntityManager em;
    @Autowired
    OrderService orderService;

    @Autowired
    OrderRepository orderRepository;


    @Test
    public void 상품주문() throws  Exception{
        // given
        Member member = createMember();

        Item book = createBook("시골 JPA", 10000, 10);

        // when
        int orderCount = 2;
        Long orderId = orderService.order(member.getId(), book.getId(), orderCount);

        // then
        Order getOrder = orderRepository.findOne(orderId);

        assertEquals("상품 주문 시 상태는 ORDER", OrderStatus.ORDER, getOrder.getStatus());
        assertEquals("주문한 상품 종류 수가 정학해야 한다",1, getOrder.getOrderItems().size());
        assertEquals("주문 가격은 가격 * 수량이다", 10000 * orderCount, getOrder.getTotalPrice());
        assertEquals("주문 수량만큼 재고가 줄어야 한다.",8, book.getStockQuantity());


    }

    @Test(expected = NotEnoughStockException.class)
    public void 상품주문_재고수량초과() throws  Exception{
        // given
        Member member = createMember();
        Item item = createBook("시골 JPA", 10000, 10);

        int orderCount = 11;
        // when
        orderService.order(member.getId(),item.getId(),orderCount);

        // then
        fail("재고 수량 부족 예외가 발생해야 한다");
    }

    @Test
    public void 주문취소() throws  Exception{
        // given
        Member member = createMember();
        Book item = createBook("시골 JPA", 10000, 10);

        int orderCount = 2;

        Long orderId = orderService.order(member.getId(),item.getId(),orderCount);

        // when
        orderService.cancelOrder(orderId);

        // then
        Order getOrder = orderRepository.findOne(orderId);
        assertEquals("주문 취소시 상태는 CANCEL이다",OrderStatus.CANCEL, getOrder.getStatus());
        assertEquals("주문이 취소된 상품은 그만큼 재고가 증가해야 한다", 10,item.getStockQuantity());

    }

    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;
    }

    private Member createMember() {
        Member member = new Member();
        member.setName("회원1");
        member.setAddress(new Address("서울","강가 ", "123-123"));
        em.persist(member);
        return member;
    }

}

반복되는 Member와 Book을 생성하는 코드를 메소드로 추출했다 ctrl+alt+m

실무에서 테스트는 훨씬 꼼꼼하고, 단위 테스트를 하는 게 더 좋다.

주문 검색 기능 개발

JPA에서 동적 쿼리를 어떻게 해결해야 할까?
회원명과 주문상태로 검색을 하기 때문에 where절에 동적 쿼리가 들어가야 한다.

파라미터 바인딩을 해서 쿼리를 짜면 되지만, 파라미터가 없을 수도 있다. 즉 회원명과 주문상태 필터 없어 쿼리를 짜야할 수도 있다. 이런 상황이 동적 쿼리가 필요한 상황이다.
첫번째 방법은 jpql로 문자열을 활용해서 이어붙이며 쿼리를 작성하는 것이다.
굉장히 복잡하고 힘든 방법으로 실무에서 사용하지 않는다.
버그를 찾기 정말 힘들고 번거롭다.

두번째 방법은 Criteria를 사용하는 방법이다.
JPA가 동적 쿼리를 작성하게 해주는 표준 방식이다.
실무에서는 마찬가지로 잘 사용하지 않는다.
유지보수성이 매우 떨어진다.

세번째 방법은 QueryDSL 라이브러리를 사용하는 것이다.
훨씬 단순하게 동적 쿼리를 작성할 수 있다.
추후에 다룬다..

0개의 댓글