[JPA] 주문 도메인 개발

이준영·2022년 10월 28일
0

스프링 - JPA

목록 보기
9/11

주문도메인 개발

이번시간에서의 두가지 키워드는 기능순서

  • 구현해야하는 기능은 다음과 같다
  1. 상품 주문
  2. 주문내역 조회
  3. 주문 취소
  • 각각의 기능을 개발하는 순서는 다음과 같다
  1. 주문&주문상품 엔티티 개발
  2. 주문 리포지토리 개발 repository
  3. 주문 서비스 개발 service
  4. 주문 검색 기능 개발
  5. 기능 테스트 Test

구현 순서는 어떤 기능 구현에서도 적용되니 꼭 기억하기❗️❗️

구현해야할 각 기능마다 개발 순서를 따라가며 하나씩 코드 작성해보자

  • 주문 엔티티 : Order

@Entity
@Table(name = "orders")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {

    @Id @GeneratedValue
    @Column(name = "order_id")  // column을 테이블명의 아이디. DB들이 이 방식을 선호
    private Long id;

    // ___ToOne 인 애들은 기본 fetch가 EAGER
    // 그래서 LAZY로 바꿔줘야함
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")  //추가로 joinColumn. 매핑을 뭘로 할거냐..?
    // Member orders와 양방향 연관관계
    // 양방향 연관관계는 관계의 주인을 정해줘야헤. Order의 회원을 바꿀떄 여기의 값을 바꿀 수 있고 반대로 Member에서 orderList의 값을 바꿀수도있어
    // 양방향 참조인데 fk를 가지고 있는건 orders!!
    // 그래서 누가 주인이라고? fk가 가까운애?
    // Order에 있는 member를 주인으로 잡아야한다는데
    // 주인이라는게  Member 개체 vs Order개체에 있는 Member 를 비교하는거였어?
    private Member member;

    //___ToMany 인 애들은 기본 fetch가 LAZY
    // cascade: 뭘 한번에 해준대..
    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY, 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)
    private OrderStatus status; // 주문상태

    //==연관관계 메서드=//
    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);
    }

    //==생성 메서드==//
    // 각각을 set, set, set.. 하는게 아니라 생성 메서드로 한번에!!
    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;
    }

    //==비즈니스 로직==//
    // 이것도 domain차원에서 비즈니스 로직 구현
    /**
     * 주문 취소
     */
    public void cancel() {
        if (delivery.getStatus() == DeliveryStatus.COMP) {
            throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
        }

        this.setStatus(OrderStatus.CANCEL);
        for (OrderItem orderItem : orderItems) {        // this를 쓰냐 안쓰냐는 알아서..
            orderItem.cancel();
        }
    }

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

}

코드의 디테일한 설명은 주석 확인하기!!

  • Order 엔티티에서 지원하는 메서드
    -createOrder() : 회원, 배송지, 주문상품 정보 받아서 주문을 생성한다.
    -cancel() : 주문을 취소한다. 이미 배송이 완료된 상품이면 취소 못하게 예외 발생시킨다(get/setStatus 사용)
    -getTotalPrice() : 전체주문 가격을 조회한다
  • 주문상품 엔티티 : OrderItem

@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 서비스에서 new Order() 이렇게 직접 생성 못하게
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;      // 주문 수량

    //==생성 메서드==//
    // 주문이 들어오면 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();
    }
}

주목할 코드 : @NoArgsConstructor(access = AccessLevel.PROTECTED)
이게 뭐냐면 서비스에서 직접 Order order1 = new Order() 이렇게 직접 생성 못하게 막아주는 annotation이다.

왜 막아줘야 되냐고??.. 다시듣자...

  • OrderItem 엔티티에서 지원하는 메서드 : 직접 구현하는 것은 없고 Order에서 구현한 메서드를 이용하여 구현한다
    -createOrder(item, price, count) : 상품, 가격, 수량을 전달받아 주문상품 엔티티를 생성하고 item.removeStock(count)를 사용하여 주문받은 수량 만큼 상품의 재고를 줄인다
    -cancel() : 주문을 취소한다. getItem().addStock(count)로 수량을 다시 돌려놓는다
    -getTotalPrice() : 주문 가격에 수량을 곱한 값을 반환한다

  • 주문 리포지토리 : 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);
    }

    // 검색기능 - 동적쿼리

다른 리포지토리와 마찬가지로 EntityManager em을 생성해준다

  • OrderRepository에서 지원하는 메서드
    -save(order) : 주문 저장
    -findOne(id) : 주문 id로 주문을 조회

    검색기능은 나중에!!


  • 주문 서비스 : OrderService

@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);     // member id 가져오기
        Item item = itemRepository.findOne(itemId);             // item id 가져오기

        // 배송정보 생성
        Delivery delivery = new Delivery();
        delivery.setAddress(member.getAddress());

        // 주문상품 생성
        // 생성 메서드가 있으니 직접 생성하지마!
        OrderItem orderItem = OrderItem.createOrderItem(item, item.getPrice(), count);

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

        //주문 저장
        // 이거는 왜 위에랑 다르게 이것만 띡 써놔도 되는거야?
        // Delivery나 OrderItem 보면 각자의 repository로 가서 save를 하던 뭘하든 어쩄든 본인 repository?로 가잖아
        // 쟤네는 cascade라서..? or private이라서..?
        // delveiry나 orderitem 같은애들은 Order에서만 쓰잖아
        // 만약 다른데에서도 갖다 쓰는애들이면 cascade 막 쓰면 안됨
        // JAP 활용_1 - '주문 서비스 개발' 편 강의
        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.findAllByString(orderSearch);
    }


}
  • 주문에서는 회원과 상품의 정보를 조회할 수 있어야 되니까 Order, Member, Item의 Repository를 모두 생성해주자

  • order 생성자에 @Transactional을 붙여주는 이유는 위에서 Transactional의 default값을 readonly로 해놨기 때문에 write가 필요한 order에서 Transaction을 write도 가능하게 다시 붙여주기 위함!!

  • OrderService에서 지원하는 메서드
    • order(member_Id, item_Id, count) : 회원ID, 상품ID, 수량을 받아서 주문 엔티티를 생성 & 저장
    • cancelOrder(order_Id) : 주문ID를 받아서 조회 후 주문 취소
    • findOrders(orderSearch) : 주문 검색

참고
CASCADE는 참조하는 애가 private owner일 때 / persist life cycle이 똑같을 때만 써라

엔티티, 리포지토리, 서비스 구조를 살펴보면 Delivery는 Order말고 아무도 안쓰고 Orderitme 도 Order만 참조해서 쓴다. 이런 애들만 ON DELETE 속성을 CASCADE로 줘야된다.

뭔소린지는 알겠는데 언제 어디서 적용해야 될지 막막하다.
==> CASCADE 막 쓰지 마라. 일단 쓰지마


도메인 모델 패턴 vs 트랜잭션 스크립트 패턴

저번 상품 도메인 개발 포스팅에서 이럴거면 service layer 왜 만드는거야.. 라고 한적이 있다.

이번 주문 서비스에서도 주문과 주문 취소 메서드를 보면 비즈니스 로직을 구현하는 메서드가 대부분 엔티티에 있다.
즉, service에서 메서드를 새로 정의하는게 아니라 엔티티 에서 이미 만든 메서드를 호출하는 방식으로 비즈니스 로직을 짜고 있단 말이지??

이런 방식을 도메인 모델 패턴 이라고 한다!!
도메인에서 이미 핵심 메서드 다 짜놨으니까 서비스야 너는 이거 가져가서 쓰기나해. 라는 식의 개발 방법이다.

반대로 엔티티에는 최소한의 메서드(Getter & Setter) 만 만들어놓고 서비스에서 메서드를 구현해서 비즈니스 로직을 짜는 방식을 트랜잭션 스크립트 패턴 이라고 한다

.
.
그렇대.. 각각의 장단점이 있는데
도메인 모델 패턴 개발이 한눈에 들어와서 유지보수가 편리해보인다!

profile
화이팅!

0개의 댓글