이번시간에서의 두가지 키워드는 기능
과 순서
다
주문
조회
취소
repository
service
Test
구현 순서는 어떤 기능 구현에서도 적용되니 꼭 기억하기❗️❗️
구현해야할 각 기능마다 개발 순서를 따라가며 하나씩 코드 작성해보자
@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;
}
}
코드의 디테일한 설명은 주석 확인하기!!
@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이다.
왜 막아줘야 되냐고??.. 다시듣자...
createOrder(item, price, count)
: 상품, 가격, 수량을 전달받아 주문상품 엔티티를 생성하고 item.removeStock(count)를 사용하여 주문받은 수량 만큼 상품의 재고를 줄인다cancel()
: 주문을 취소한다. getItem().addStock(count)로 수량을 다시 돌려놓는다getTotalPrice()
: 주문 가격에 수량을 곱한 값을 반환한다@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로 주문을 조회
검색기능은 나중에!!
@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를 모두 생성해주자@Transactional
을 붙여주는 이유는 위에서 Transactional의 default값을 readonly로 해놨기 때문에 write가 필요한 order에서 Transaction을 write도 가능하게 다시 붙여주기 위함!!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
막 쓰지 마라. 일단 쓰지마
저번 상품 도메인 개발 포스팅에서 이럴거면 service layer 왜 만드는거야..
라고 한적이 있다.
이번 주문 서비스에서도 주문과 주문 취소 메서드를 보면 비즈니스 로직을 구현하는 메서드가 대부분 엔티티에 있다.
즉, service에서 메서드를 새로 정의하는게 아니라 엔티티 에서 이미 만든 메서드를 호출하는 방식으로 비즈니스 로직을 짜고 있단 말이지??
이런 방식을 도메인 모델 패턴
이라고 한다!!
도메인에서 이미 핵심 메서드 다 짜놨으니까 서비스야 너는 이거 가져가서 쓰기나해. 라는 식의 개발 방법이다.
반대로 엔티티에는 최소한의 메서드(Getter & Setter) 만 만들어놓고 서비스에서 메서드를 구현해서 비즈니스 로직을 짜는 방식을 트랜잭션 스크립트 패턴
이라고 한다
.
.
그렇대.. 각각의 장단점이 있는데
도메인 모델 패턴 개발이 한눈에 들어와서 유지보수가 편리해보인다!