// 주문
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.of(item, item.getPrice(), count);
// 주문 생성
Order order = Order.of(member, delivery, orderItem);
// 주문 저장
orderRepository.save(order);
return order.getId();
}
// 주문상품 생성
OrderItem orderItem = OrderItem.of(item, item.getPrice(), count);
// 주문 생성
Order order = Order.of(member, delivery, orderItem);
위의 코드는 주문과 주문상품 엔티티 객체를 생성 메소드를 이용하여 생성했습니다.
OrderItem orderItem = new OrderItem();
orderItem.setItem(item);
orderItem.setCount(count);
...
그런데 만약 다른 개발자가 이 생성 메소드의 존재를 모르고 위와 같이 코드를 짜면 어떻게 될까요? 일단 코드가 하드코딩이고, 어떨 때는 생성메소드를 사용하고 어떨 때는 위의 코드같이 짜니 유지보수에도 좋지 않을 것 같습니다. 그래서 생성 메소드 사용을 강제하는 것이 좋습니다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderItem {
...
}
생성 메소드 사용을 강제하는 방법은 기본생성자의 접근제어자를 Protected로 바꾸는 것입니다. 위의 코드는 롬복을 이용하여 구현한 것입니다. 접근제어자가 Protected이므로 OrderItem orderItem = new OrderItem();
같은 코드는 작성할 수 없게 됩니다. 따라서 생성메소드의 사용이 강제가 되는 것입니다.
주문 서비스의 주문 저장 메소드를 보면 orderItem 객체와 delivery 객체를 분명히 생성했음에도 불구하고 order만 save를 함을 볼 수 있습니다.
응??? orderItem과 delivery는 save를 안해도 되는건가??? 라고 생각할 수 있습니다.
이에 대한 해답은 다음 코드에 있습니다.
public class Order {
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(mappedBy = "order", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Delivery delivery;
}
order 엔티티를 보시면 orderItems와 delivery에 cascade = CascadeType.ALL이 붙어 있는 것을 볼 수 있습니다. 이 의미는 orderItem, delivery는 order와 생명주기를 같이한다는 의미입니다. 좀 더 자세히 말하면 order가 save되면 orderItem과 delivery 또한 저장되고 order가 삭제되면 orderItem과 delivery 또한 삭제된다는 의미입니다.
이처럼 cascade는 편리한 기능을 제공하는 것은 사실입니다. 하지만 조심해서 사용해야합니다. 왜냐하면 만약 delivery를 다른 엔티티도 참조를 하고 있는데 order가 삭제되서 delivery가 삭제되면 안되기 때문입니다.
이 경우에는 주문과 주문상품의 경우 생명주기를 같이하는 것은 맞기 때문에 cascade를 사용하였습니다.
// 주문 취소
public void cancel(long orderId) {
// 엔티티 조회
Order order = orderRepository.findOne(orderId);
// 주문 취소
order.cancel();
}
public void cancel(long orderId) {
Order order = orderRepository.findOne(orderId);
if(order.delivery.getStatus() == DeliveryStatus.COMP) {
throw new IllegalStateException();
}
order.setStatus(OrderStatus.CANCEL);
order.orderItems.forEach(it -> {
it.item.addStock(it.count);
})
}
만약 서비스에서 비즈니스 로직을 처리한다면 위의 코드와 같을 것입니다. 위의 코드는 절차지향적인 코드입니다. 위의 코드는 순서에 따라서 비즈니스 로직을 처리하는 코드이기 때문입니다.
// 주문 취소
public void cancel(long orderId) {
// 엔티티 조회
Order order = orderRepository.findOne(orderId);
// 주문 취소
order.cancel();
}
위의 코드에서 DDD의 진가를 알 수 있습니다. 위의 코드는 order라는 객체에게 취소를 하라는 메세지를 보냅니다. 객체에게 메세지를 보내서 처리하게 하는 것 이것이 객체지향적인 코드입니다.
또한 우리는 order라는 객체가 어떻게 취소를 하는지 비즈니스 로직을 알 필요가 없습니다. 그냥 위의코드를 보면 아~ order 객체가 취소를 처리했구나 라는 것만 알뿐입니다. 이것이 바로 캡슐화입니다.
public void cancel() {
if(this.delivery.getStatus() == DeliveryStatus.COMP) {
throw new IllegalStateException(CANT_CANCEL_MESSAGE);
}
this.status = OrderStatus.CANCEL;
this.orderItems.forEach(OrderItem::cancel);
}
public void cancel() {
this.item.addStock(this.count);
}
원래는 주문 취소를 할 때 위의 코드에서 보면 알다시피 order의 상태와 item의 상태가 변하므로 update 쿼리를 날려줘야 합니다. 그런데 주문 서비스의 주문 취소 메소드를 보면 어디를 봐도 update 쿼리를 날려주는 메소드가 존재하지 않습니다. 이는 바로 JPA의 변경감지 덕분인데 JPA는 영속성 컨텍스트가 관리하는 엔티티의 상태가 변경하면 변경을 감지하여 자동으로 update 쿼리를 날려주기 때문입니다.
public class OrderItem {
private OrderItemDao orderItemDao;
public void cancel() {
this.item.addStock(this.count);
orderItemDao.updateStock(item);
}
}
만약 마이바티스를 사용했다면 updateStock 메소드와 같이 update 쿼리를 날려주는 메소드를 사용해야 했을 것입니다. 이렇게 되면 문제가 되는 것이 도메인에서 DAO 객체를 참조하는 일이 벌어지게 되는데 이는 설계가 잘못된 것입니다. 따라서 DDD를 하기 힘들게 됩니다.
하지만 JPA는 변경감지 덕분에 updateStock 같은 메소드가 필요없게 되고 덕분에 DDD를 하기 훨씬 수월해졌습니다.