주문과 취소가 중요합니다. 그리고 검색도 되어야합니다.
지금까지 만든 것들을 어떻게 엮는지 알아봅시다.
package jpabook.jpashop.service;
import jpabook.jpashop.domain.Delivery;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderItem;
import jpabook.jpashop.domain.item.Item;
import jpabook.jpashop.repository.ItemRepository;
import jpabook.jpashop.repository.MemberRepository;
import jpabook.jpashop.repository.OrderRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final MemberRepository memberRepository;
private final ItemRepository itemRepository;
/**
* 주문 ...1
*/
@Transactional
public Long order(Long memberId, Long itemId, int count) { //...a
//엔티티 조회
Member member = memberRepository.findOne(memberId);
Item item = itemRepository.findOne(itemId);
//배송정보 생성
Delivery delivery = new Delivery();
delivery.setAddress(member.getAddress()); //...b
//주문상품 생성
OrderItem orderItem = OrderItem.createOrderItem(item, item.getPrice(), count);
//주문 생성
Order order = Order.createOrder(member, delivery, orderItem); //...d
//주문 저장
orderRepository.save(order); //...c
return order.getId();
}
/**
* 주문 취소 ...2
*/
@Transactional
public void cancelOrder(Long orderId) {
//주문 엔티티 조회
Order order = orderRepository.findOne(orderId);
//주문 취소
order.cancel();
}
//검색 ...3
// public List<Order> findOrders(OrderSearch orderSearch) {
// return orderRepository.findAll(orderSearch);
// }
}
주문하려면 member와 item이 필요하고 몇개 주문할 것인지 count가 필요합니다.
위의 그림처럼 id, id, 수량이 넘어옵니다. 그래서 값을 꺼내야하니 리포지토리가 필요합니다.
배송정보 생성
보면 delivery도 따로 생성했고, OrderItem도 따로 생성했습니다.
원래라면 delivery 리포지토리가 있어서 save로 따로 넣어주고, OrderItem도 JPA에 넣어준 다음에 createOrder에 세팅을 해야합니다.
그런데 orderRepository.save(order);
만 했습니다.
이게 가능한 이유는 CASCADE 옵션 때문입니다.
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(fetch = LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
Order를 persist하면 들어와 있는 컬렉션들도 강제로 persist를 날려줍니다.
그래서 orderRepository.save(order);
하나만 저장을 해줘도 orderItem과 delivery가 자동으로 persist가 된 것입니다.
<aside> ❗ CASCADE에 대한 범위는 많은 사람들이 고민합니다. (어디까지 CASCADE를 지정해야할지 고민합니다.)
보통 명확하게 말하기는 애매합니다. 예를 들어, Order가 delivery와 orderItem를 관리합니다. 이런 그림정도에서만 써야합니다. 즉, 참조하는 주인이 프라이빗 오너인 경우에만 써야합니다.
delivery는 Order말고 아무대서도 안씁니다. orderItem도 Order만 참조해서 씁니다. 물론 orderItem이 다른 것을 참조하긴 하지만 다른 곳에서 orderItem를 참조하진 않습니다.
그러니 라이프 사이클을 동일하게 관리할 때 의미가 있고 다른 것이 참조할 수 없는 프라이빗 오너인 경우 CASCADE를 쓰면 도움을 받을 수 있습니다.
하지만 그게 아니라면 CASCADE를 막 사용하면 안됩니다. 별도의 리포지토리를 생성해서 save를 별도로 하는게 낫습니다.
이번 케이스는 Order만 Delivery를 사용하고 Order만 OrderItem을 사용하고 persist해야되는 라이프 사이클이 완전히 똑같기 때문에 2가지 조건이 충족해서 CASCADE를 썼습니다.
참고로 예제를 간단하게 하기 위해서 주문 상품을 하나만 넘기도록 했습니다.
하지만 주문 생성을 할 때 다른 주문상품을 생성해서 넘기면 여러개 주문이 됩니다.
Order order = Order.*createOrder*(member, delivery, orderItem, orderItem...);
<aside>
❗ 혼자 개발할 때는 createOrderItem를 만들어놓고 호출을 하겠지만 누군가는 OrderItem orderItem = new OrderItem();
하고 set.~~으로 값을 채우면서 개발할 수 있습니다.
이렇게 되면 문제가 어떤 곳에선 생성 메서드를 쓰고 어디서는 new로 생성하게되면 나중에 생성 로직을 변경할 때 유지보수하기 굉장히 어려워집니다. 필드를 넣거나 로직을 추가할 때 분산되기 때문입니다.
그래서 생성 메서드 생성 외의 다른 방법을 다 막아야합니다.
생성자를 만들 때 protected로 만들어줍니다. JPA는 protected까지 기본 생성자로 만들 수 있게 스펙상 허용해줍니다.
참고로 JPA 는 기본 생성자가 필수죠. → 자세히
그래서 new로 생성하려고 하면 컴파일 오류가 발생하게 됩니다. 그래서 쓰지 말라는 의도를 알 수 있습니다.
JPA를 쓰면서 protected를 쓰면 쓰지 말라는 것입니다.
@Entity
@Getter @Setter
public class OrderItem {
...
**protected OrderItem() {
}**
...
}
추가로 여기서 롬복을 사용해서 더 줄일 수 있습니다.
@Entity
@Getter @Setter
**@NoArgsConstructor(access = AccessLevel.PROTECTED)**
public class OrderItem {
...
}
기본 생성자를 PROTECTED로 생성한다는 어노테이션입니다. 실무에서 많이 사용합니다.
Sets the access level of the constructor. By default, generated constructors are public.
설명에도 나와있듯 기본은 public으로 만들어준다고 합니다.
마찬가지로 Order 엔티티도 @NoArgsConstructor(access = AccessLevel.PROTECTED)를 넣어줍시다. → 참고1, 참고2
항상 코드를 제약하는 스타일로 짜는게 좋습니다. 좋은 설계와 유지보수를 끌어갈 수 있습니다.
</aside>
<aside> ❗ @NoargsConstructor(AccessLevel.PROTECTED) 와 @Builder
[참고]
주문 취소
주문을 취소할 때 id만 넘어옵니다.
그래서 먼저 id로 찾아야합니다.
그 다음에 order.cancel();
하면 끝납니다.
로직이 엄청 간단합니다. 그 이유는 이미 cancel이라는 비즈니스 로직을 만들었습니다.
Order의 cancel을 호출하면 배송 완료가 된 것은 취소가 안되기 때문에 예외를 터트리고 그게 아니라면 상태를 cancel로 바꾸고 재고 수량을 원복해야하기 때문에 orderItem.cancel을 호출합니다. 그러면 orderItem.cancel에서 재고를 count만큼 올려줍니다.
<aside> ❗ JPA의 강점이 여기서 나옵니다.
일반적으로 데이터베이스 sql을 직접 다루는 mybatis나 jdbcTemplate같은 경우를 봅시다.
order.cancel()에서 데이터를 변경했습니다. this.setStatus(Orderstatus.CANCEL);
변경을 하면 밖에서 직접 update 쿼리를 짜서 날려줘야합니다.
마찬가지로 orderItem.cancel() 후에 재고가 올라가야하니 +해주는 sql을 직접 짜서 올려야합니다.
그러니 로직을 바꾸고 나서 데이터를 끄집어내서 쿼리에 파라미터 넣어서 따로 처리해야합니다. 그러니 트랜잭셔널 스크립트라고 하는데 서비스 계층에서 비즈니스 로직을 다 쓸 수 밖에 없습니다.
그런데 JPA를 활용하면 데이터만 바꾸면 JPA가 알아서 바뀐 변경 포인트들을 더티 체킹(변경 감지)이 일어나면서 변경된 내역들을 다 찾아서 데이터베이스에 update 쿼리가 착착 날라갑니다.
즉, Order에 update 쿼리가 날라가고, Item에도 update 쿼리가 날라가서 stockQuantity가 원복이 될 것입니다.
이게 JPA를 사용할 때 엄청나게 큰 장점입니다.
</aside>
주문 검색
<aside> ❗ 주문 서비스의 주문과 주문 취소 메서드를 보면 비즈니스 로직 대부분이 엔티티에 있습니다.
단순하게 엔티티를 조회하고 연결하고 호출해주는 정도만 하는 것입니다.
즉, 서비스 계층 은 단순히 엔티티에 필요한 요청을 위임하는 역할을 합니다. 이처럼 엔티티가 비즈니스 로직을 가지고 객체 지향의 특성을 적극 활용하는 것을 도메인 모델 패턴(https://martinfowler.com/eaaCatalog/domainModel.html)이라 합니다.
반대로 엔티티에는 비즈니스 로직이 거의 없고 서비스 계층에서 대부분 의 비즈니스 로직을 처리하는 것을 트랜잭션 스크립트 패턴(http://martinfowler.com/eaaCatalog/ transactionScript.html)이고 합니다. (일반적으로 SQL 다룰 때 패턴)
JPA를 사용하면 도메인 모델 패턴으로 코딩을 많이 하게 됩니다.
하지만, 서로의 트레이드 오프가 존재하고 문맥에 맞춰서 알맞은 패턴을 사용하면 됩니다.
</aside>