Entity, 리포지토리, 서비스 개발
주문, 주문 상품 Entity 개발
생성 메서드
핵심 로직을 도메인에 넣는다.
연관 관계에 따라 생성을 복잡하게 해야한다면 생성 메서드를 만든다.
생성하는 로직을 바꿀 때 여기만 손대면 된다.
@Entity
@Getter
@Setter
public class 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;
}
}
createOrderItem에서 이미 removeStock()으로 재고를 깐 상태로 orderItem이 넘어오게 된다.
주문 취소
@Entity
@Getter
@Setter
@Table(name = "orders")
public class Order {
...
// 비즈니스 로직
/**
* 주문 취소
*/
public void cancel() {
if (delivery.getStatus() == DeliveryStatus.COMP) {
throw new IllegalStateException("이미 배송 완료된 상품은 취소가 불가능합니다.");
}
this.setStatus(OrderStatus.CANCEL);
// IDE 에디터 색 때문에 정말 this를 꼭 명시적으로 보여줘야할 때가 아니면
// this.orderItems보다 orderItems를 선호한다.
for (OrderItem orderItem : orderItems) {
// 주문한 아이템들도 다 취소를 해줘야 한다.
orderItem.cancel();
}
}
}
마찬가지로 담당 도메인에서 로직을 처리하도록 한다.
전체 주문 가격 조회
@Entity
@Getter
@Setter
@Table(name = "orders")
public class Order {
...
// 조회 로직
/**
* 전체 주문 가격 조회
*/
public int getTotalPrice() {
return orderItems.stream().mapToInt(OrderItem::getTotalPrice).sum();
}
}
전체 주문 가격을 알려면 주문 상품 각각의 가격을 알아야 한다.
orderItem에서 가격을 조회한 다음 그 가격을 합한다.
실무에서는 주로 주문에 전체 주문 가격 필드를 추가하는 역정규화 방식을 쓴다.
주문 리포지토리 개발
@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);
}
}
주문 서비스 개발
주문
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
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) {
// Entity 조회
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);
// order만 repository에 저장
orderRepository.save(order);
return order.getId();
}
}
@Entity
@Getter
@Setter
@Table(name = "orders")
public class Order {
...
@OneToMany(mappedBy = "order", cascade = ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(fetch = LAZY, cascade = ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
}
orderItem, delivery가 cascade=ALL로 지정되어 있기 때문에 order만 persist 하면 다 같이 쿼리를 날려준다.
cascade 범위
다른 곳에서 참조하지 않으면서 라이프 사이클을 동일하게 관리해야 한다면 cascade를 적용한다.
order가 orderItem과 delivery를 관리한다.
order 외에는 orderItem과 delivery를 참조하지 않기 때문이다.
delivery나 orderItem이 중요한 도메인이라서 다른 곳에서도 쓰게 된다면 절대 사용해선 안된다.
생성 로직
OrderItem orderItem=OrderItem.createOrderItem(item,item.getPrice(),count);
OrderItem orderItem=new OrderItem();
orderItem.setCount();
생성 로직을 따로 만들어놔도 다른 개발자가 사용하지 않으면 유지 보수가 어렵다.
@Entity
@Getter
@Setter
public class OrderItem {
protected OrderItem() {
}
}
생성자를 protected로 해두면 생성 방법이 무분별하게 많아지는 것을 막을 수 있다.
@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderItem {
}
롬복을 사용할 수도 있다.
주문 취소
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class OrderService {
/**
* 주문 취소
*/
@Transactional
public void cancelOrder(Long orderId) {
// 주문 Entity 조회
Order order = orderRepository.findOne(orderId);
// 주문 취소
order.cancel();
}
}
JPA는 더티 체킹으로 변경된 내용을 감지해 자동으로 쿼리를 날려준다.
원래는 order.cancel()의 OrderStatus와 orderItem.cancel()의 count 변경 사항도 각각 저장해줘야 한다.
도메인 모델 패턴
Entity가 비즈니스 로직을 가지고 객체 지향을 적극 활용하는 패턴이다.
예제를 보면 비즈니스 로직 대부분이 Entity에 있다.
서비스 계층은 단순히 Entity에 필요한 요청을 위임하는 역할을 한다.
트랜잭션 스크립트 패턴
Entity에 비즈니스 로직이 거의 없고 서비스 계층에서 대부분의 로직을 처리한다.
Last updated
Was this helpful?