실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 섹션6. 주문 도메인 개발

2023. 9. 11. 23:41·CS/김영한 스프링 강의

이제 아이템을 주문할 때 창고에 있는 아이템 개수는 줄어들고, 주문목록엔 생겨나고, 장바구니 역할을 하는 OrderItem의 총 가격 등을 계산하고.. 같은 핵심 비즈니스를 해볼거다.

 

@Entity
@Table(name = "orders")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // protected로 생성자를 막음. createOrder를 사용하도록 함.
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) // ORDINAL은 숫자로 들어감. STRING으로 쓰자.
    private OrderStatus status; // 주문상태 [ORDER, CANCEL]

    //==연관관계 메서드==//
    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) { // ...으로 받은 orderItems를 for문으로 돌림.
            order.addOrderItem(orderItem);
        }
        order.setStatus(OrderStatus.ORDER); // 주문상태를 ORDER로 설정.
        order.setOrderDate(LocalDateTime.now()); // 주문시간을 현재시간으로 설정.
        return order;
    }

    //==비즈니스 로직==//
    /**
     * 주문 취소
     */
    public void cancel() {
        if (delivery.getStatus() == DeliveryStatus.COMP) { // 배송완료 상태면 취소 불가.
            throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
        }
        this.setStatus(OrderStatus.CANCEL); // 주문상태를 CANCEL로 설정.
        for (OrderItem orderItem : orderItems) { // 주문상품들을 for문으로 돌림.
            orderItem.cancel(); // 주문상품들을 취소시킴.
        }
    }

    //==조회 로직==//
    /**
     * 전체 주문 가격 조회
     */
    public int getTotalPrice() { // 전체 주문 가격을 구하는 메서드.
        // orderItems를 stream으로 돌림.
        // mapToInt로 OrderItem의 getTotalPrice를 뽑아냄. sum으로 더함.
        return orderItems.stream().mapToInt(OrderItem::getTotalPrice).sum();

    }

}

엔터티의 속성들이라 깊게 연관되어 있어서 엔터티 함수로 해야되는데 복잡한거면 static를 사용해서 언제든 사용할 수 있게 하는게 좋음. 여기선 주문 만들기인데 그래서 주문 만들 땐 생성자 따로 쓰지 말고 저걸 사용해서 하기. 그래서 아얘 못사용하게 생성자를 protected로 막는게.

 

@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 orderItem = new OrderItem();
        orderItem.setItem(item); // OrderItem에 Item을 넣음.
        orderItem.setOrderPrice(orderPrice); // OrderItem에 주문 가격을 넣음.
        orderItem.setCount(count); // OrderItem에 주문 수량을 넣음.

        item.removeStock(count); // Item에서 주문 수량만큼 재고를 뺌.
        return orderItem;
    }

    //==비즈니스 로직==//
    public void cancel() {
        getItem().addStock(count);
    }

    public int getTotalPrice() {
        return getOrderPrice() * getCount();
    }
}
@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);
//    }
}

서비스의 간단한 함수지만 주목해야 할 건 두가지가 있다.

하나는 분명 OrderItem, Order, Delivery들은 각각의 테이블을 만들기 때문에 각각 persist로 save를 해줘야 할 것 같은데, 그냥 order의 save 하나로 저 3가지가 모두 테이블 db에 저장된다는 것. 이유는 cascadeType.ALL을 설정해주었기 때문임. 저 함수를 실행하는 동안 변화를 감지하고 기억했다가 저장할 때 한번에 다 같이 저장한다. 그러나 cascade로 묶여 있기 때문에 삭제할 때도 한번에 삭제되서 만약 다른곳에서도 참고하고 있는 내용이었는데 갑자기 사라지면 곤란해질 수 있다. 그러니 처음엔 안쓰다가 익숙해지면 쓰는걸 추천한다.

또 하나는 서비스는 그냥 함수 순서대로 쭉 불러와서 실행하는것 밖에 없어서 사실상 비즈니스 핵심 기능은 언티티 도메인에 몰빵되어 있는데 이른 도메인 모델 패턴이라고 함. 반대로 서비스에 집중되어 있는걸 트랜잭션 스크립트 패턴이라고 한다. 어느게 더 좋다는게 아니고 자기 프로젝트에 따라 뭐가 더 좋은지는 다르고, 둘 다 양립할 수도 있다.

 

이제 이렇게 작성한 걸 테스트하는 테스트 코드를 작성해보자.

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

    @Autowired
    EntityManager em;
    @Autowired
    OrderService orderService;
    @Autowired
    OrderRepository orderRepository;

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

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

        int orderCount = 2;

        // when
        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
    public void 상품주문_재고수량초과() {
        // given
        Member member = createMember();
        Book book = createBook("시골 JPA", 10000, 10);

        int orderCount = 11;

        // when
        Assertions.assertThatThrownBy(() -> {
            orderService.order(member.getId(), book.getId(), orderCount);
        }).isInstanceOf(NotEnoughStockException.class);

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

    }

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

    }

    @Test
    public void 상품주문_예산초과() throws Exception {
        // given

        // when

        // then
    }


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

사실 이건 강의용이고 전체적으로 잘 돌아가는지 통합 테스트라서 그렇지, 유닛테스트 별로 독립적으로 나눠서 테스트 하는게 좋다.

 

검색 기능을 추가 하려고 해보자.

일단 이렇게 적었는데 조건이 없으면?? 그래서 동적쿼리를 해야하는데 if문을 짜서 만약 조건이 없으면 어쩌구~ 해서 string을 만들던지, 표준 스택인 jpa creteria을 해서 하던지 하는 방법이 있는데 둘 다 사람이 할 게 아니다. 그래서 querydsl 추천함.

'CS > 김영한 스프링 강의' 카테고리의 다른 글

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 섹션 2. JPA 시작하기  (0) 2023.09.14
실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 섹션7. 웹 계층 개발  (0) 2023.09.14
실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 섹션5. 상품 도메인 개발  (0) 2023.09.09
실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 섹션4. 회원 도메인 개발  (1) 2023.09.08
실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 섹션2. 도메인 분석 설계  (0) 2023.09.07
'CS/김영한 스프링 강의' 카테고리의 다른 글
  • 자바 ORM 표준 JPA 프로그래밍 - 기본편 - 섹션 2. JPA 시작하기
  • 실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 섹션7. 웹 계층 개발
  • 실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 섹션5. 상품 도메인 개발
  • 실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 섹션4. 회원 도메인 개발
용나리
용나리
  • 용나리
    티스토리 블로그
    용나리
  • 전체
    오늘
    어제
    • 분류 전체보기 (333)
      • 과거의 것들 (93)
        • AI Tech boostcamp (92)
      • 생각정리(고찰) (2)
      • 기술 글 (0)
      • 코딩테스트 (4)
        • C++ (0)
        • Python (4)
      • CS (121)
        • 컴퓨터 시스템 (4)
        • 코틀린 인 액션 (13)
        • 김영한 스프링 강의 (104)
      • 일지 남기기용 (11)
        • 운동 (10)
      • 개발 배포 해보기 (1)
      • 프로그래밍 언어 및 기타 (32)
        • Spring Boot (9)
        • Python (9)
        • Kotlin (1)
        • Flutter (2)
        • SQL (4)
        • Docker (3)
        • 공통 (4)
      • os (4)
        • Linux (4)
      • 기술 (17)
        • PyTorch (6)
        • Computer Vision (6)
        • NLP (1)
        • 기타 (4)
      • 제품 후기 (0)
      • 게임 (0)
        • Human Resource Machine (0)
      • 강의 (26)
        • fullstackdeeplearning_sprin.. (9)
        • 부캠 안드로이드 학습정리 (17)
      • 개인 메모 (10)
      • IT 기타 (5)
      • 논문 읽기 연습 (5)
        • Computer Vision (1)
        • NLP (0)
        • 공통 (2)
        • 그냥 메모 (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    파이썬 실행경로
    pip install killed
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
용나리
실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 섹션6. 주문 도메인 개발
상단으로

티스토리툴바