구현 기능
- 상품 주문
- 주문 내역 조회
- 주문 취소
순서
- 주문 엔티티, 주문 상품 엔티티 개발
- 주문 리포지토리 개발
- 주문 서비스 개발
- 주문 검색 기능 개발
- 주문 기능 테스트
주문, 주문상품 엔티티 개발
Order Entity
package jpabook.jpashop.domain;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.*;
import javax.persistence.*;
@Entity
@Table(name = "Orders")
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 기본 생성자를 protected로 선언하여 다른 패키지의 클래스에서 빈생성자를 생성을 막음
public class Order {
...
//==생성 메서드==//
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.getDeliveryStatus() == DeliveryStatus.COMP){
throw new IllegalStateException("이미 배송완료한 상품은 취소가 불가능합니다.");
}
this.setStatus(OrderStatus.CANCLE);
for(OrderItem orderItem : orderItems){
orderItem.cancel();
}
}
//==조회 로직==//
/**
* 전체 주문 가격 조회
*/
public int getTotalPrice() {
return orderItems.stream()
.mapToInt(OrderItem::getTotalPrice)
.sum();
/*
위와 같은 코드
int totalPrice = 0;
for (OrderItem orderItem : orderItems) {
totalPrice += orderItem.getTotalPrice();
}
return totalPrice;*/
}
}
OrderItem Entity
package jpabook.jpashop.domain;
import jpabook.jpashop.domain.Item.Item;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 기본 생성자를 protected로 선언하여 다른 패키지의 클래스에서 빈생성자를 생성을 막음
public class OrderItem {
...
//==생성 메서드==//
public static OrderItem createOrderItem(Item item, int orderPirce, int count){
OrderItem orderItem = new OrderItem();
orderItem.setItem(item);
orderItem.setOrderPirce(orderPirce);
orderItem.setCount(count);
item.removeStock(count);
return orderItem;
}
//==비즈니스 로직==//
public void cancel() {
getItem().addStock(count);
}
public int getTotalPrice(){
return getOrderPirce()*getCount();
}
}
주문 리포지토리 개발
OrderRepository
package jpabook.jpashop.repository;
import jpabook.jpashop.domain.Order;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import java.util.*;
import javax.persistence.EntityManager;
@Repository
@RequiredArgsConstructor
public class OrderRepository {
private final EntityManager em;
public void save(Order order){
em.persist(order);
}
public Order findOne(Long orderId){
return em.find(Order.class, orderId);
}
//후에 개발
//public List<Order> findAll(OrderSearch orderSearch) {}
}
주문 서비스 개발
OrderService
package jpabook.jpashop.service;
import jpabook.jpashop.domain.Delivery;
import jpabook.jpashop.domain.Item.Item;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderItem;
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;
import java.util.*;
@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){
// 엔티티 조회
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);
// 주문 취소 (비즈니스 로직을 만들어 놓았기 때문에, cancel 메소드만 접근하면 된다.)
order.cancel();
// jpa가 아니라면 수정된 데이터들을 update 쿼리문을 날려야하지만,
// jpa는 더티 체킹을 통해 변경된 내역을 알아서 update 쿼리문을 날린다.
// 정말 편리
}
/**
* 검색
*/
//public List<Order> findOrders(OrderSearch orderSearch){
// return orderRepository.findAll(orderSearch);
//}
}
jpa가 아니라면 order.cancel() 아래 코드에서 변경된 데이터에 대한 업데이트 쿼리문을 작성하여 DB에 날려야 하지만, jpa의 경우 더티체킹을 통해 기존의 데이터와 바뀐 데이터가 있다면 알아서 찾아내어 update 쿼리문을 날리기때문에 따로 update 쿼리문을 작성하여 날리지 않아도 됩니다!!! (정말 편리하네요..)
참고
엔티티에 핵심 비즈니스 로직을 적는 패턴을 "도메인 모델 패턴"이라고 부릅니다. "도메인 모델 패턴"은 서비스 계층을 단순히 엔티티에 필요한 요청을 위임하는 역할을 합니다.
반대로, 엔티티에는 비즈니스 로직이 거의 없고, 서비스 계층에서 대부분의 비즈니스 로직을 처리하는 것을 "트랜잭션 스크립트 패턴"이라고 합니다.
두 가지 방법이 있는 것은 어느 패턴이 더 뛰어나기 때문이 아닌 상황에 따라서, 유지 보수에 좋은 상황이 있기 때문입니다.
결론 : 상황에 따라서 유지 보수하기 편리한 패턴을 사용하자!
주문 기능 테스트
OrderServiceTest
package jpabook.jpashop.service;
import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Item.Book;
import jpabook.jpashop.domain.Item.Item;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderStatus;
import jpabook.jpashop.exception.NotEnoughStockException;
import jpabook.jpashop.repository.OrderRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import static org.junit.Assert.*;
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class OrderServiceTest {
@Autowired
private EntityManager em;
@Autowired OrderService orderService;
@Autowired
OrderRepository orderRepository;
@Test
public void 상품주문() throws Exception {
//given
Member member = createMember();
Item book = createBook("영진 JPA", 10000, 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("주문 수량만큼 재고가 줄어야 한다.", 10-orderCount, book.getStockQuantity());
}
@Test
public void 주문취소() throws Exception {
//given
Member member = createMember();
Item book = createBook("영진 JPA", 10000, 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());
}
@Test(expected = NotEnoughStockException.class)
public void 재고수량초과() throws Exception {
//given
Member member = createMember();
Item book = createBook("영진 JPA", 10000, 10);
int orderCount = 11;
//when
Long orderId = orderService.order(member.getId(), book.getId(), orderCount); // 여기서 예외가 터져야함.
//then
fail("재고 수량 부족 예외가 발생해야 한다.");
}
// 만들어 놓은 로직을 ctrl+alt+M 하면 메소드화 가능
private Item createBook(String name, int price, int stockQuantity) {
Item book = new Book();
book.setName(name);
book.setPrice(price);
book.setStockQuantity(stockQuantity);
em.persist(book);
return book;
}
private Member createMember() {
Member member = new Member();
member.setName("회원1");
member.setAddress(new Address("서울", "경기", "123-123"));
em.persist(member);
return member;
}
}
위의 코드는 DB와 연동해서 테스트를 하는 예제입니다.
참고
도메인 모델 패턴은 도메인 엔티티에 비즈니스 로직이 있기때문에 도메인으로만 로직이 정상적으로 작동하는지 테스트하는 것이 장점입니다. 이를 잘 활용하는 것이 도메인 모델 패턴을 보다 잘 살려내는 방법이라고 생각합니다. 이유는 DB와 연동하지 않기때문에 테스트가 빠르기 때문입니다.
주문 검색 기능 개발
JPA에서 동적 쿼리를 어떻게 해결해야 하는가?
- JPA Criteria
- 유지보수성이 너무 떨어진다. 직접 쿼리를 작성해보면 무슨 쿼리가 작성되는지 머리로 그려지지 않기 때문입니다.
- Querydsl
- 동적 쿼리를 정말 강력하게 해결할 수 있다.
- 강력하게라 함은 자바코드이기 때문에 컴파일 시점에서 오타를 다 잡아줍니다.
- 또한, 코드가 간결하고 유지보수성이 뛰어납니다.
복잡한 동적 쿼리는 Querydsl로 해결하는 것을 권장합니다.
Querydsl에 대한 설명은 후에 함.
출처
'spring > 인프런 강의 정리' 카테고리의 다른 글
[JPA 활용 2] 1. API 개발 기본 (0) | 2022.04.20 |
---|---|
[JPA 활용 1] 7. 웹 계층 개발 (0) | 2022.04.19 |
[JPA 활용 1] 5. 상품 도메인 개발 (0) | 2022.04.18 |
[JPA 활용 1] 4. 회원 도메인 개발 (0) | 2022.04.18 |
[JPA 활용 1] 3. 애플리케이션 구현 준비 (0) | 2022.04.17 |