06 주문 도메인 개발 - Order Entity, Repository, Service

shin·2023년 9월 24일
0

[ 구현 기능 ]

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

[ 구현 순서 ]

  • 주문 엔티티, 주문상품 엔티티 개발 > 핵심 비즈니스 로직 추가
  • 주문 리포지토리 개발
  • 주문 서비스 개발
  • 주문 검색 기능 개발
  • 주문 기능 테스트

1. Order/OrderItem Entity + 비즈니스 로직 추가

1) Order Entity

...
@Entity
@Table(name = "orders")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
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(DeliveryStatus.COMP.equals(this.getDelivery().getStatus())){
            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;
    }

}

2) OrderItem Entity

...
@Entity
@Table(name = "order_item")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
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();
    }

}


3) 기능 상세 설명

createOrder() - 생성 메서드

OrderEntity

    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;
    }
  • 주문 엔티티를 생성할 때 사용
  • 주문회원, 배송정보, 주문상품의 정보를 받아서 실제 주문 엔티티를 생성함
  • 연관관계가 있는 정보들을 하나의 메서드에서 세팅함으로써, 추후 주문 생성과 관련하여 세팅이 바뀌어도 메서드 내부 로직만 변경하면 됨

createOrderItem() - 생성 메서드

OrderItem Entity

    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;
    }
  • 주문 상품, 가격, 수량 정보를 사용해서 주문상품 엔티티를 생성
  • removeStock()을 호출해서 주문한 수량만큼 상품의 재고를 줄임

💡 AessLevel.PROTECTED 설정

  • 생성 메서드를 만들어놓았기 때문에, new OrderItem()을 사용해서 새로운 주문 상품을 생성하지 못하도록 해야함
    protected OrderItem(){
        
    }
    protected Order(){
        
    }
  • 위 코드처럼 entity에 코드를 직접 추가하는 방법도 있지만, Lombok 어노테이션을 활용하면 더 간단하게 표현 가능
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderItem {
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {
  • AccessLevelProtected로 설정하면 서비스 단에서 new로 주문 상품 객체를 생성할 수 없도록 막을 수 있음

cancel() - 비즈니스 로직

order Entity

    public void cancel(){
        if(this.getDelivery().getStatus().equals(DeliveryStatus.COMP)){
            throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
        }

        this.setStatus(OrderStatus.CANCEL);

        for(OrderItem orderItem : orderItems){
            orderItem.cancel();
        }
    }
  • 주문 취소시 사용
  • 주문 상태를 취소로 변경하고, 주문 상품에 주문 취소를 알림
  • 만약 이미 배송을 완료한 상품이라면 주문 취소가 불가능하도록 예외를 발생시킴

OrderItem Entity

    public void cancel() {
        getItem().addStock(count);
    }
  • addStock()을 호출해서 취소한 주문 수량만큼 상품의 재고를 증가시킴

getTotalPrice() - 조회 로직

Order Entity

    public int getTotalPrice(){
        int totalPrice = 0;

        for (OrderItem orderItem : orderItems){
            totalPrice += orderItem.getTotalPrice();
        }

        return totalPrice;
    }
  • 주문 시 사용한 전체 주문 가격을 조회
  • 전체 주문 가격을 알기 위해서는 주문 상품의 가격을 알아야 하기 때문에, 주문 상품들의 가격을 조회하여 더한 값을 반환함
  • 실무에서는 주로 주문에 전체 주문 가격 필드를 두고 역정규화하여 구현

OrderItem Entity

    public int getTotalPrice(){
        return getOrderPrice() * getCount();
    }
  • 주문 상품의 가격은 각 주문 상품의 가격과 주문 수량을 곱한 값을 반환

2. Order Repository

package jpabook.jpashop.repository;

import jakarta.persistence.EntityManager;
import jpabook.jpashop.domain.Order;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

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 id){
        return em.find(Order.class, id);
    }
    
//    public List<Order> findAll(OrderSearch orderSearch){}
    
}
  • 저장, 단건 조회 구현
  • 검색 기능은 추후 구현될 예정

3. Order Service 개발

  • 주문 서비스는 주문 엔티티와 주문 상품 엔티티의 비즈니스 로직을 활용해서 주문, 주문 취소, 주문 내역 검색 기능을 제공

1) Order Service

package jpabook.jpashop.service;

import jpabook.jpashop.domain.*;
import jpabook.jpashop.repository.ItemRepository;
import jpabook.jpashop.repository.MemberRepository;
import jpabook.jpashop.repository.OrderRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepository;
    private final MemberRepository memberRepository;
    private final ItemRepository itemRepository;

    /**
     * 주문 저장
     * @param memberId, itemId, count
     * @return Long
     */
    @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();
    }

    /**
     * 주문 취소
     * @param orderId
     */
    @Transactional
    public void cancelOrder(Long orderId){
        //주문 엔티티 조회
        Order order = orderRepository.findOne(orderId);

        //주문 취소
        order.cancel();
    }

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

}


2) 기능 상세 설명

order()

    @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();
    }
  • 주문하는 회원 식별자, 상품 식별자, 주문 수량 정보를 받아서 실제 주문 엔티티를 생성한 후 저장

💡 order만 save하는 이유

Order Entity

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();

    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;
  • Cascade를 설정해놓았기 때문에, order가 persist될 때 orderItem과 delivery도 강제로 persist를 해줌

Order Service

	 @Transactional
    public Long order(Long memberId, Long itemId, int count){
        ...
        //주문 저장
        orderRepository.save(order);
        ...
    }
  • 따라서 order만 save해도 자동으로 orderItem과 delivery도 persist가 됨
  • Order만 OrderItem과 Delivery를 사용하고, Persist해야 하는 Life Cycle도 완전히 동일하기 때문에 위 방식으로 구현이 가능함
    • 이렇게 다른것이 참조하지 않는 프라이빗 오너인 경우에 이런 방식을 사용하면 도움을 받을 수 있음
    • 만약 다른 곳에서도 참조를 한다면 cascade를 사용하면 안되고, 별도의 리포지토리를 생성해서 따로 관리를 해줘야 함

canelOrder()

    @Transactional
    public void cancelOrder(Long orderId){
        //주문 엔티티 조회
        Order order = orderRepository.findOne(orderId);

        //주문 취소
        order.cancel();
    }
  • 주문 식별자를 받아서 주문 엔티티를 조회한 후 주문 엔티티에 주문 취소를 요청
  • JPA가 변경 감지를 통해 변경된 부분을 감지해서 데이터베이스에 자동으로 업데이트 쿼리를 날려줌

findOrders()

  • OrderSearch라는 검색 조건을 가진 객체로 주문 엔티티를 검색
  • 추후 주문 검색 기능 구현 파트에서 더 자세한 내용 확인 가능


💡 아키텍처 패턴

도메인 모델 패턴

  • 주문 서비스의 주문, 주문 취소 메서드를 보면 비즈니스 로직이 대부분 엔티티에 존재함
  • 서비스 계층은 단순히 엔티티에 필요한 요청을 위임하는 역할을 함
  • 이처럼 엔티티가 비즈니스 로직을 가지고 객체 지향의 특성을 적극 활용하는 것을 도메인 모델 패턴이라고 함
  • 참고 링크 : https://martinfowler.com/eaaCatalog/

트랜잭션 스크립트 패턴

  • 엔티티에는 비즈니스 로직이 거의 없고 서비스 계층에서 대부분의 비즈니스 로직을 처리하는 것
  • 참고 링크 : https://martinfowler.com/eaaCatalog/


강의 : 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발

profile
Backend development

0개의 댓글