스프링데이터 + JPA/웹 애플리케이션 개발

17. 주문 리포지토리, 서비스

sdafdq 2023. 11. 7. 12:40

리포지토리

@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);
    }
    
//    public List<Order> findAll(OrderSearch orderSearch){}

}

간단하게 주문 저장,

주문 하나 가져오기.

그런데 여러개 가져오는데, 검색 옵션에 따라 가져오기.

저건 동적 쿼리가 필요해서 나중에 따로 함.

 

서비스

기능 요구 사항은

주문 조회

주문

주문 취소

 

먼저 주문

주문할 때 필요한 건,

 

member, delivery, 또 주문아이템, 그에따라 item도 필요하다.

 

@Service
@Transactional
@RequiredArgsConstructor
public class OrderService {
    private final OrderRepository orderRepository;
    private final MemberRepository memberRepository;
    private final ItemRepository itemRepository;
    
    //주문
    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();
    }
}

member 가져오고,

주문할 item 가져온다. 주문아이템으로 만들기 위해.

 

배송 생성을 한다.

원래 배송지 새로 받던지 해서 저것도 아예 orderDto등 만들어서 받는 게 맞지만,

그냥 간략하게 member의 주소로.

 

item으로 orderItem을 만든다.

가격도 뭐 할인이나 이런 거 있을 수 있기에 orderPrice가 따로 있는거지만, 지금은 그냥 상품의 가격으로 한다.

또 OrderItem도, 여기서 생성하는 게 아니라 아니면 저걸 따로 orderItemDto로 또 만들어서 orderDto에 넣던지 해서,

item, count, orderPrice 등이 있는 List<OrderItemDto> 로 하는 게 좋아보인다.

 

그 다음 이제 최종적으로 주문을 생성해준다.

저렇게 하고 생성된 주문을 save 해 준다.

그럼 다 등록이 된다.

우리가 delivery, orderItem 등을 save(persist) 하지 않았지만,

@Entity(name = "orders")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {
    @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;


    public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems){
        Order order = new Order();
        order.changeMember(member);
        order.changeDelivery(delivery);
        for (OrderItem orderItem : orderItems) {
            order.addOrderItem(orderItem);
        }
        order.setStatus(OrderStatus.ORDER);
        order.setOrderDate(LocalDateTime.now());
        return order;
    }
}

여기 보면 cascade, 즉 영속성 전파를 ALL로 해놨기 때문에,

orderItems와 delivery는 

order를 persist 하면 그것이 전파되어 같이 persist 된다.

 

저 @NoArgsConstructor는 빈 인자 생성자를 만들어 주는 거 인데, JPA에서는 PROTECTED 레벨까지 허용해 주므로 거기까지 접근 권한을 설정한다.

여튼 이런 식으로 항상 제약적으로 하는 게 유지보수를 좋게 한다.

 

근데, 저 service의 order에서

public Long order(Long memberId, List<OrderItemDto> orderItemsDto){
    List<OrderItem> orderItems = new ArrayList<>();
    
    for( OrderItemDto orderItemDto : orderItemsDto){
        Item item = itemRepository.findOne(orderItemDto.itemId);
        int orderPrice = orderItemDto.price;
        int count = orderItemDto.count;
        
        OrderItem orderItem = OrderItem.createOrderItem(item, orderPrice, count);
        orderItems.push(orderItem);
    }
}

이렇게 하는 게 맞지 않을까 함.

컨트롤러에서 여러 아이템 선택해서 주문하면

그거 각각 따로따로 저렇게 list로 받아서

 

저렇게 하면 되지 않을까 함.

 

 

public void cancelOrder(Long orderId){
    Order order = orderRepository.findOne(orderId);

    order.cancel();
}

이렇게 주문 찾아서 그거 cancel 해주면 끝.

 

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

    this.setStatus(OrderStatus.CANCEL);
    for (OrderItem orderItem : orderItems) {
        orderItem.cancel();
    }
}

이미 구현해 놨음. 엔티티에.

배송완료된거면 에러 날리고,

아니면 상태를 cancel로 바꾸고 주문상품들도 다 cancel함.

public void cancel(){
    this.getItem().addStock(count);
}

주문 아이템의 cancel은 그냥 재고 원래대로 더해주는 거임.

 

 

 

 

@Service
@Transactional
@RequiredArgsConstructor
public class OrderService {
    private final OrderRepository orderRepository;
    private final MemberRepository memberRepository;
    private final ItemRepository itemRepository;


    //주문
    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();
    }


    // 취소
    public void cancelOrder(Long orderId){
        Order order = orderRepository.findOne(orderId);

        order.cancel();
    }

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

}

지금 보면 서비스에서 대부분 도메인의 로직을 호출함.

비즈니스 로직이 도메인 안에 있음.

서비스 계층은 단순히 엔티티에 필요한 요청을 위임하는 역할.

이처럼 엔티티가 객체지향 특성을 적극활용하는 것을 도메인 모델 패턴이라고 함.

 

반대로 엔티티에 로직 거의 없고 서비스 계층에서 로직 처리하는 것을 트랜잭션 스크립트 패턴 이라고 함.

트랜잭션 스크립트 패턴은 저 도메인 안에 있는 비즈니스 로직이 서비스에 있다고 생각하면 됨.

 

뭐가 더 맞냐 틀리냐는 없음. 서로 트레이드 오프가 있음. 문맥 상 맞는  거 쓰면 됨. 그냥 둘다 쓰면 됨. 

 

 

 

지금은 스프링이랑 연동해서 테스트 할 수 있구나, 해서 한 거고.

 

정말 좋은 테스트는 단위 테스트는

mocking하고 DB 안묶고

 

도메인 모델 패턴의 테스트 할 때 장점은,

도메인 안에 비즈니스 로직이 있기 때문에 그것이 잘 작동 하는 지 딱 그것만 단위테스트 하기 좋다.

 

 

'스프링데이터 + JPA > 웹 애플리케이션 개발' 카테고리의 다른 글

19. 주문 검색 기능 개발  (0) 2023.11.09
18. 주문 기능 테스트  (0) 2023.11.08
16. 주문 도메인 개발  (0) 2023.11.07
15. 상품 개발  (0) 2023.11.07
14. 회원 테스트  (0) 2023.11.06