2 분 소요


상품 도메인 개발 순서: 상품 Entity 개발(비즈니스 로직 추가) -> 상품 Repository -> 상품 service -> test case


Item(Entity)

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DisCriminatorColumn(name = "dtype")
@Getter @Setter
public abstract class Item {

    // Entity 설계 부분 생략...
    ...

    //==비스니스 로직==//
    public void addStock(int quantity) {
        // 주문 취소시 재고 수량을 다시 추가
        this.stockQuantity += quantity;
    }

    public void removeStock(int quantity) {
        // 주문시 재고에서 주문 수량만큼 감소
        int restStock = this.stockQuantity - quantity;
        if(restStock < 0) {
            throw new NotEnoughStockException("need more Stock");
        }
        this.stockQuantity = restStock;
    }
}
  • addStock(): 파라미터로 넘어온 수 만큼 재고를 늘린다. 이 메서드는 재고가 증가하거나 상품 주문을 취소해서 재고를 다시 늘려야 할 때 사용한다.
  • removeStock(): 파라미터로 넘어온 수 만큼 재고를 줄인다. 만약 재고가 부족하면 예외가 발생한다. 주로 상품을 주문할 때 사용.

예외 추가

public class NotEnoughStockException extends RuntimeException {

    // override...
    ...
}


ItemRepository

@Repository
@RequiredArgsConstructor
public class ItemRepository {

    private final EntityManager em;

    public void save(Item item) {
        if(item.getId() == null) {
            em.persist(item);
        } else {
            em.merge(item);
        }
    }

    public Item findOne(Long id) {
        return em.find(Item.class, id)
    }

    public List<Item> findAll() {
        return em.createQuery("select i from Item i", Item.class)
                .getResultList();
    }
}
  • save()
    • id가 없으면 신규로 보고 persist() 실행
    • id가 있으면 이미 데이터베이스에 저장된 엔티티를 수정한다고 생각하고, merge() 실행.
      • merge()대신 변경 감지 기능을 사용하는 것을 권장!

상품 테스트는 회원 테스트와 비슷하기 때문에 생략..


주문 도메인 개발

개발 순서: 주문 Entity, 주문상품 Entity 개발 -> 주문 Repository -> 주문 Service -> 주문 검색 기능 개발 -> 주문 기능 테스트

Order(Entity)

@Entity
@Table(name = "orders")
@Getter @Setter
public class Order {

    // Entity 설계 부분 생략...
    ...

    //==생성 메서드==//
    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;
    }
}

OrderItem(Entity)

@Entity
@Table(name = "order_item")
@Getter @Setter
public class 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;
    }

    //== 비즈니스 로직==//

    // 주문 취소
    public void cancel() {
        getItem().addStock(count);
    }

    // 조회 로직
    public int getTotalPrice() {
        return getOrderPrice() * getCount();
    }
}

OrderRepository

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

OrderService

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

    private final MemberRepository memberRepository;
    private final OrderRepository orderRepository;
    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());
        delivery.setStatus(DeliveryStatus.READY);

        // 주문상품 생성
        OrderItem orderItem = OrderItem.createOrderItem(item, item.getPrice(), count);

        // 주문 생성
        Order order = Order.createOrder(member, delivery, orderItem);

        // 주문 저장
        orderRepositroy.save(order);
        return order.getId();
    }

    // 주문 취소
    @Transactional
    public void cancelOrder(Long orderId) {

        Order order = orderRepository.findOne(orderId);

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

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


<출처 : 인프런 - 실전! 스프링 부트와 JPA 활용1 (김영한)>

댓글남기기