프로그래밍/spring boot

[스프링부트] 실전! 스프링 부트와 JPA 활용1 #6 주문 도메인 개발(1)

aSpring 2023. 11. 3. 11:22
728x90
728x90
※ 본 포스팅은 김영한 강사님의 인프런 '실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발' 강의를 들으며 작성한 수강일지 입니다.

 

| 주문 도메인 개발

1. 주문, 주문상품 엔티티 개발
2. 주문 리포지토리 개발
3. 주문 서비스 개발
4. 주문 기능 테스트
5. 주문 검색 기능 개발

 

구현 기능
  • 상품 주문
  • 주문 내역 조회
  • 주문 취소

 

1. 주문, 주문상품 엔티티 개발

1) 주문 엔티티

package jpabook.jpashop.domain;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

import static javax.persistence.FetchType.LAZY;

@Entity
@Table(name = "orders") // 관례상 예약어 order로 table명이 생성되지 않게 orders로 적어줌
@Getter
@Setter
public class Order {

    @Id @GeneratedValue
    @Column(name = "order_id")
    private Long id;

    // member와 관계 세팅 -> 양방향 -> 연관관계의 주인을 정해주어야 함(값이 바뀌면 뭘 보고 해야하나? FK가 있는 쪽을 연관관계의 주인으로 설정)
//    @ManyToOne(fetch = FetchType.EAGER) // 이게 기본인데 LAZY로 꼭 변경하기!
    @ManyToOne(fetch = LAZY) // xToONE은 전부 찾아서 LAZY로 변경
    @JoinColumn(name = "member_id") // mapping을 뭘로 할 것이냐 => member_id가 FK가 됨
    private Member member;

//    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY) // 기본 전략이 LAZY
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) // 기본 전략이 LAZY이므로 굳이 적어주지 않아도 됨
    private List<OrderItem> orderItems = new ArrayList<>();

    // 원래는 아래처럼 다 해주어야 하지만 CascadeType.ALL을 해주면, cascade는 persist를 전파하기 때문에 persist(order)만 남기면 됨, ALL로 해두어 delete 할 때도 같이 지워버림
//    persist(orderItemA)
//    persist(orderItemB)
//    persist(orderItemC)
//    persist(order)

    @OneToOne(fetch = LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;

    // private Date data; // date 관련된 annotation 줘야했는데 java 8에서는 아래 사용하면 됨
    // order_date
    private LocalDateTime orderDate;

    @Enumerated(EnumType.STRING)
    private OrderStatus status; // 주문 상태 [ORDER, CANCEL]

    // 양방향 연관관계 세팅
    //==연관관계 (편의) 메서드==// // -> 핵심적으로 control 하는 쪽에 코드가 위치하는 것이 좋음
    public void setMember(Member member) {
        this.member = member;
        member.getOrders().add(this); // 아래 두 과정을 놓칠 수 있으니 이걸 원자적으로 묶어주는 메서드 만들어 넣기
    }

//    public static void main(String[] args) {
//        Member member = new Member();
//        Order order = new Order();
//
////        member.getOrders().add(order);  // member.getOrders().add(this); 위에 이 코드 덕분에 이 코드는 쓰지 않아도 됨
//        order.setMember(member);
//    }

    // order <-> orderItems도 양방향
    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(); // item 재고 원복시킴
        }
    }

    //==조회 로직==//
    /**
     * 전체 주문 가격 조회
     */
    public int getTotalPrice() {
//        int totalPrice = 0;
//        for (OrderItem orderItem : orderItems) {
//            totalPrice += orderItem.getTotalPrice();
//        }
//        return totalPrice;

        //위와 같은 로직
        return orderItems.stream()
                .mapToInt(OrderItem::getTotalPrice)
                .sum();
    }
}

기능 설명

  • 생성 메서드 createOrder() : 주문 엔티티를 생성할 때 사용. 주문 회원, 배송정보, 주문상품의 정보를 받아서 실제 주문 엔티티를 생성함.
  • 주문 취소 cancel() : 주문 취소 시 사용. 주문 상태를 취소로 변경하고 주문상품에 주문 취소를 알림. 만약 이미 배송을 완료한 상품이면 주문을 취소하지 못하도록 예외를 발생시킴.
  • 전체 주문 가격 조회: 주문 시 사용한 전체 주문 가격을 조회함. 전체 주문 가격을 알려면 각각의 주문상품 가격을 알아야 함. 로직을 보면 연관된 주문상품들의 가격을 조회해서 더한 값을 반환함.(실무에서는 주로 주문에 전체 주문 가격 필드를 두고 역정규화를 함)

2) 주문상품 엔티티

package jpabook.jpashop.domain;

import jpabook.jpashop.domain.item.Item;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;

import static javax.persistence.FetchType.LAZY;

@Entity
@Getter @Setter
public class OrderItem {

    @Id @GeneratedValue
    @Column(name = "order_item_id")
    private Long id;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "item_id")
    private Item item;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "order_id")
    private Order order;

    private int orderPrice; //주문 가격
    private int count; //주문 수량

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

기능 설명

  • 생성 메서드 createOrderItem() : 주문 상품, 가격, 수량 정보를 사용해서 주문상품 엔티티를 생성함. item.removeStock(count)를 호출해서 주문한 수량만큼 상품의 재고를 줄임
  • 주문 취소 cancel() : getItem().addStock(count)를 호출해서 취소한 주문 수량만큼 상품의 재고를 증가시킴.
  • 주문 가격 조회 getTotalPrice() : 주문 가격에 수량을 곱한 값을 반환함.


2. 주문 리포지토리 개발

package jpabook.jpashop.repository;

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

import javax.persistence.EntityManager;
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) {}
}

주문 리포지토리에는 주문 엔티티를 저자하고 검색하는 기능이 있음.

마지막의 findAll(OrderSearch orderSearch)메서느는 주문 검색 기능에서 자세히 설명할 것.


3. 주문 서비스 개발

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

 

cascade를 어디까지 사용할 것인가?
- 참조하는 것이 주인이 private owner인 경우에만 사용(delivery, orderItem은 order만 참조해서 사용하므로 cascade 사용)
ex) order가 delivery를 관리하고, order가 orderItem을 관리함 -> cascade OOOOOO
ex) delivery가 중요해서 다른 데서도 참조, 갖다쓴다면 -> cascade XXXXXX

 

생성을 각각 다른 스타일로 하는 것을 막기 위해 스타일 고정하기

1) protected로 해서 분산을 방지하기 위해 생성 방법을 고정해 주면 우측처럼 new ~~ 했을 때 compile 에러가 나도록 뜨게 된다

 

2) 또는 lombok 이용

 

원래라면 transactional 생 쿼리를 짤 때, 데이터 바꾸고 나서 나와서 파라미터값 넣는 등 처리를 통해서 DB 값을 변경시켜 주어야 하는데, JPA를 사용하면 dirty checking(변경 내역 감지)을 통해 값 변경이 일어나면 DB update 쿼리를 알아서 날려줌 -> 값을 알아서 바꾸어 줌

 

참고: 예제를 단순화하려고 한 번에 하나의 상품만 주문할 수 있다.

 

  • 주문( order() ): 주문하는 회원 식별자, 상품 식별자, 주문 수량 정보를 받아서 실제 주문 엔티티를 생성한 후 저장함.
  • 주문 취소( cancelOrder() ): 주문 식별자를 받아서 주문 엔티티를 조회한 후 주문 엔티티에 주문 취소를 요청한다.
  • 주문 검색( findOrders() ): OrderSearch 라는 검색 조건을 가진 객체로 주문 엔티티를 검색한다. 자세한 내용은 다음에 나오는 주문 검색 기능에서 알아보자.
참고: 주문 서비스의 주문과 주문 취소 메서드를 보면 비즈니스 로직 대부분이 엔티티에 있다.
서비스 계층 은 단순히 엔티티에 필요한 요청을 위임하는 역할을 한다.
이처럼 엔티티가 비즈니스 로직을 가지고 객체 지 향의 특성을 적극 활용하는 것을 도메인 모델 패턴(https://martinfowler.com/eaaCatalog/domainModel.html)이라 한다.
반대로 엔티티에는 비즈니스 로직이 거의 없고 서비스 계층에서 대부분 의 비즈니스 로직을 처리하는 것을 트랜잭션 스크립트 패턴(https://martinfowler.com/eaaCatalog/transactionScript.html)이라 한다.

우리가 하고있는 것은 도메인 모델 패턴(JPA, ORM..)

일반적으로 SQL문 쓸 때 하는 패턴은 트랜잭션 스크립트 패턴

-> 둘 다 장단점이 있고 사용하기 더 적절한 때가 있으며, 한 프로젝트 안에서도 문맥에 따라 둘을 혼용해서 양립해서 사용하기도 함

728x90
728x90