구현 기능
1.
상품 주문
2.
주문 내역 조회
3.
주문 취소
주문, 주문상품 엔티티 개발
주문(Order)엔티티 코드(생성메서드, 비즈니스 로직)
@Entity
@Table(name = "ORDERS")
@Getter @Setter
public class Order {
@Id
@GeneratedValue
@Column(name = "order_id")
private Long id;
private LocalDateTime orderDate;
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="member_id")
private Member member;
@OneToMany(mappedBy = "order",cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@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);
}
//==생성 메서드==//
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;
}
//==비즈니스 로직==//
/**
* 주문 취소
*/
public void cancel(){
if(delivery.getStatus()==DeliveryStatus.COMP){
throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
}
this.setStatus(OrderStatus.CANCEL);
for(OrderItem orderItem : orderItems){
orderItem.cancel();
}
}
//==조회 로직 ==//
/**
* 전체 주문 가격 조회
*/
public int getTotalPrice(){
int totalPrice = 0;
for(OrderItem orderItem : orderItems){
totalPrice += orderItem.getTotalPrice();
}
return totalPrice;
}
}
Java
복사
주문상품 엔티티 코드
@Entity
@Table(name = "order_item")
@Getter @Setter
public class OrderItem {
@Id
@GeneratedValue
@Column(name = "order_item_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "item_id")
private Item item;
private int orderPrice;
private int count;
//==생성 메서드==//
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();
}
}
Java
복사
주문 리포지토리 개발
@RequiredArgsConstructor
@Repository
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);
}
// public List<Order> findAll(OrderSearch orderSearch){}
}
Java
복사
주문 서비스 개발
@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);
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);
//주문 저장
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){
}*/
}
Java
복사
•
주문(order): 주문하는 회원 식별자, 상품 식별자, 주문 수량 정보를 받아 실제 주문 엔티티를 생성 후 저장.
•
주문 취소(cancelOrder()): 주문 식별자를 받아서 주문 엔티티 조회 후 주문 엔티티에 주문 취소를 요청한다.
•
주문 검색(findOrders()): OrderSearch라는 검색 조건을 가진 객체로 주문 엔티티를 검색한다.
참고: 위 주문 서비스(OrderService)의 대부분의 비즈니스 로직이 각각 모델(엔티티)에 있는걸 알 수 있다. 서비스 계층에서는 단순히 엔티티에 필요한 요청을 위임하는 역할을 한다. 이와 같이 비즈니스 로직을 엔티티에서 가지고 있는 것은 객체지향의 특성을 활용하는 것을
도메인 모델 패턴(http://martinfowler.com/eaaCatalog/domainModel.html)이라 한다.
반대로, 엔티티에는 비즈니스 로직이 거의 없고 서비스계층에서 대부분의 비즈니스 로직을 처리하는 것을 트랜잭션 스크립트 패턴(http://martinfowler.com/eaaCatalog/transactionScript.html)이라 한다.
주문 기능 테스트
테스트 요구사항
•
상품 주문이 성공해야 한다.
•
상품을 주문할 때 재고 수량을 초과하면 안 된다.
•
주문 취소가 성공해야 한다.
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class OrderServiceTest {
@Autowired
EntityManager em;
@Autowired
OrderService orderService;
@Autowired
OrderRepository orderRepository;
@Test
public void 상품주문() throws Exception{
//given
Member member = createMember("회원1", new Address("서울", "강가", "123-123"));
Book book = createBook(10000, "시골 JPA", 10);
int orderCount = 2;
//when
Long orderId = orderService.order(member.getId(), book.getId(), orderCount);
//then
Order getOrder = orderRepository.findOne(orderId);
assertEquals("상품 주문시 상태는 ORDER", OrderStatus.ORDER, getOrder.getStatus());
assertEquals("주문한 상품 종류 수가 정확해야 한다.", 1, getOrder.getOrderItems().size());
assertEquals("주문 가격은 가격 * 수량이다.", 10000*orderCount, getOrder.getTotalPrice());
assertEquals("주문 수량만큼 재고가 줄어야 한다." ,8, book.getStockQuantity());
}
@Test(expected = NotEnoughStockException.class)
public void 상품주문_재고수량초과() throws Exception{
//given
Member member = createMember("회원1", new Address("서울", "강가", "123-123"));
Book book = createBook(10000, "시골 JPA", 10);
int orderCount = 11;
//when
orderService.order(member.getId(), book.getId(),orderCount);
//then
fail("재고 수량 부족예외가 발생해야 한다.");
}
@Test
public void 주문취소() throws Exception{
//given
Member member = createMember("회원A", new Address("서울", "강가", "123-123"));
Book book = createBook(10000, "시골JPA", 10);
int orderCount = 2;
Long orderId = orderService.order(member.getId(), book.getId(), orderCount);
//when
orderService.cancelOrder(orderId);
//then
Order getOrder = orderRepository.findOne(orderId);
assertEquals("주문 취소시 상태는 CANCEL 이다.", OrderStatus.CANCEL, getOrder.getStatus());
assertEquals("주문이 취소된 상품은 재고가 증가해야 한다.", 10, book.getStockQuantity());
}
private Book createBook(int price, String name, int stockCount) {
Book book = new Book();
book.setName(name);
book.setPrice(price);
book.setStockQuantity(stockCount);
em.persist(book);
return book;
}
private Member createMember(String s, Address address) {
Member member = new Member();
member.setName(s);
member.setAddress(address);
em.persist(member);
return member;
}
}
Java
복사
주문 검색 기능 개발
Issue_ 동적 쿼리는 어떻게 해결해야 하는가
JPA로 동적쿼리를 해결하는 방법은 크게 세가지가 있다.
1.
문자열 조합 → 조건에 따라 문자열을 결합하면서 query문을 만들고 parameter를 세팅해주는 방법
2.
JPA Criteria → JPA 표준 스펙에서 제공하는 기능.
3.
queryDSL → 오픈소스를 통해 제공되는 기능으로 쿼리구현을 method로 한다.
문자열 조합
public List<Order> findAllByString(OrderSearch orderSearch) {
String jpql = "select o from Order o join o.member m";
boolean isFirstCondition = true;
//주문 상태 검색
if(orderSearch.getOrderStatus() != null){
if(isFirstCondition){
jpql +=" where";
isFirstCondition = false;
} else {
jpql += " and";
}
jpql += " o.status = :status";
}
//회원 이름 검색
if(StringUtils.hasText(orderSearch.getMemberName())){
if(isFirstCondition){
jpql += " where";
isFirstCondition = false;
} else {
jpql += " and";
}
jpql += " m.name like :name";
}
TypedQuery<Order> query = em.createQuery(jpql, Order.class).setMaxResults(1000);
if(orderSearch.getOrderStatus() != null){
query = query.setParameter("status", orderSearch.getOrderStatus());
}
if(StringUtils.hasText(orderSearch.getMemberName())){
query = query.setParameter("name", orderSearch.getMemberName());
}
return query.getResultList();
}
Java
복사
JPA Criteria
/**
* JPA Criteria
*/
public List<Order> findAllCriteria(OrderSearch orderSearch){
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Order> cq = cb.createQuery(Order.class);
Root<Order> o = cq.from(Order.class);
Join<Object, Object> m = o.join("member", JoinType.INNER);
List<Predicate> criteria = new ArrayList<>();
//주문 상태 검색
if(orderSearch.getOrderStatus() != null){
Predicate status = cb.equal(o.get("status"), orderSearch.getOrderStatus());
criteria.add(status);
}
//회원 이름 검색
if(StringUtils.hasText(orderSearch.getMemberName())){
Predicate name =
cb.like(m.get("name"), "%"+orderSearch.getMemberName()+"%");
criteria.add(name);
}
cq.where(cb.and(criteria.toArray(new Predicate[criteria.size()])));
TypedQuery<Order> query = em.createQuery(cq).setMaxResults(1000);
return query.getResultList();
}
Java
복사
정리
→ 문자열 조합 방식은 코드 작성중 쿼리에 오타가 나도 컴파일 시점에 발견하기 힘들다는 단점과 코드가 너무 스파게티처럼 복잡해진다는 단점이 있다.
→ JPA Criteria는 사용법이 너무 어렵고 유지보수 난이도가 너무 높아 사실상 실무에서 적용이 힘들다.
→ queryDSL은 오픈소스라 다음 Lecture에서 따로 다루도록 한다.
→ 실무에서 위 세가지 방법이 다 어렵다면 NativeQuery를 사용하거나, case when 문법을 사용하여 해결할 수 있다. 하지만 문자열 조합 방식과 같이 오타를 컴파일 시점에서 잡아내기 힘들고 코드 검증이 힘들기 때문에 추천하는 방식은 아니다.