구현 기능은 상품 주문, 주문 내역 조회, 주문 취소가 있다.
구현 순서는 주문 엔티티, 주문상품 엔티티 개발 -> 주문 리포지토리 개발 -> 주문 서비스 개발 -> 주문 검색 기능 개발 -> 주문 기능 테스트 순이다.
주문, 주문상품 엔티티 개발
- 주문 생성 메서드
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;
}
Order 엔티티에 주문 생성 매서드를 만들어 한번에 주문을 만든다.
- 비즈니스 로직
/**
* 주문 취소
*/
public void cancel(){
if(delivery.getStatus() == DeliveryStatus.COMP){
throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
}
this.setStatus(OrderStatus.CANCEL);
for (OrderItem orderItem : orderItems) {
orderItem.cancel();
}
}
주문 취소 로직이다. 주문이 이미 배송 완료된 상태이면 최소를 못하게 했고 아니면 취소 상태로 바꿔주었다.
주문이 취소되면 재고를 올려야하므로 orderItem에 cancel이라는 비즈니스 로직을 만들었다.
//orderItem Entity
public void cancel() { getItem().addStock(count); }
- 조회 로직
/**
* 전체 주문 가격 조회
*/
public int getTotalPrice() {
int totalPrice = 0;
for (OrderItem orderItem : orderItems) {
totalPrice += orderItem.getTotalPrice();
}
return totalPrice;
}
전체 주문 가격을 조회하는 것이다. 모든 orderItem의 가격을 가져와 totalPrice에 더해주었다.
//orderItem Entity
public int getTotalPrice() { return getOrderPrice() * getCount(); }
- 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;
}
주문 리포지토리 개발
@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);
}
//public List<Order> findAll(){ }
}
주문 서비스 개발
- 주문 하기
@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();
}
Order와 OrderItem을 직접 생성하게 되면 여러 개발자가 사용한다고 할 때 여러 방식으로 만들어지는 문제가 생길 수 있다. 그래서 생성 메서드를 만들고 위에 아래의 코드를 추가해준다. 그러면 그냥 생성으로는 만들 수 없다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
- 주문 취소
@Transactional
public void cancelOrder(Long orderId){
//주문 엔티티 조회
Order order = orderRepository.findOne(orderId);
//주문 취소
order.cancel();
//변경 내역 감지로 인해 데이터베이스에 쿼리가 자동으로 나간다. JPA의 강점
}
- 도메인 모델 패턴 : 엔티티가 비즈니스 로직을 가지고 객체 지향의 특성을 적극 활용하는 것(위의 예제)
- 트랜잭션 스크립트 패턴 : 엔티티에는 비즈니스 로직이 거의 없고 서비스 계층에서 대부분의 비즈니스 로직을 처리하는 것
주문 기능 테스트
테스트할 기능
- 상품 주문이 성공해야 한다
- 상품을 주문할 때 재고 수량을 초과하면 안 된다.
- 주문 취소가 성공해야한다.
- 상품 주문
@Test
public void 상품주문() throws Exception{
Member member = new Member();
member.setName("회원1");
member.setAddress(new Address("서울", "강가", "123-123"));
em.persist(member); //회원 생성
Book book = new Book();
book.setName("시골 JPA");
book.setPrice(10000);
book.setStockQuantity(10);
em.persist(book); //책 생성
int orderCount = 2;
Long orderId = orderService.order(member.getId(), book.getId(), orderCount);// 책 주문
Order getOrder = orderRepository.findOne(orderId); // 주문 가져오기
assertEquals(OrderStatus.ORDER, getOrder.getStatus(), "상품 주문시 상태는 ORDER");
assertEquals(1, getOrder.getOrderItems().size(), "주문한 상품 종류 수가 정확해야 한다.");
assertEquals(10000*orderCount, getOrder.getTotalPrice(), "주문 가격은 가격 * 수량이다");
assertEquals(8, book.getStockQuantity(), "주문 수량만큼 재고가 줄어야 한다.");
}
결과가 성공적으로 나온 모습이다.
Member member = new Member();
member.setName("회원1");
member.setAddress(new Address("서울", "강가", "123-123"));
em.persist(member); //회원 생성
Book book = new Book();
book.setName("시골 JPA");
book.setPrice(10000);
book.setStockQuantity(10);
em.persist(book); //책 생성
이 코드들을 각각 ctrl - alt - m을 하면 인텔리제이가 하나의 매소드로 만들어준다. createMember와 createBook으로 만들었다.
- 재고 수량 초과 테스트
@Test(expected = NotEnoughStockException.class)
public void 상품주문_재고수량초과() throws Exception{
Member member = createMember();
Item item = createBook("시골 JPA", 10000, 10);
int orderCount = 11;
orderService.order(member.getId(), item.getId(), orderCount);
fail("재고 수량 부족 예외가 발생해야 한다.");
}
지금 재고가 10개가 있는데 만약 11개를 주문했다고 하면 order에서 에러가 발생해야 한다.
expected = NotEnoughStockException.class
이 부분이 이 에러가 발생했는지 알려준다.
에러가 성공적으로 발생하였다.
- 주문 취소
@Test
public void 주문취소() {
Member member = createMember();
Book item = createBook("시골 JPA", 10000, 10);
int orderCount = 2;
Long orderId = orderService.order(member.getId(), item.getId(), orderCount);
orderService.cancelOrder(orderId);
Order getOrder = orderRepository.findOne(orderId);
assertEquals(OrderStatus.CANCEL, getOrder.getStatus(), "주문 취소시 상태는 CANCEL 이다.");
assertEquals(10, item.getStockQuantity(), "주문 취소된 상품은 그만큼 재고가 증가해야 한다.");
}
주문 취소시 검사해야할 부분은 2개이다. 취소 되면 상태가 CANCEL로 바뀌는지 또 재고가 원 상태로 복구가 되는지이다.
테스트 결과 모두 성공하였다.
주문 검색 기능 개발
검색을 할려면 JPA에서 동적 쿼리를 사용해야 한다.
public List<Order> findAll(OrderSearch orderSearch){
//language=JPQL
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); //최대 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();
}
jpql은 기본 상태에서 검색이다.
- 처음은 주문 상태 검색으로 만약 주문이 첫번째이면 where문 만 추가하고 주문이 여러개이면 and를 붙여 자기 필요한 status를 모두 꺼내온다.
- 두번째는 회원 이름 검색이다. 주문과 똑같이 원하는 회원의 주문이 한개이면 where만 붙이고 만약 원하는 회원이 주문이 여러개라면 and를 붙여 모두 조사한다.
- TypedQuery<Order> query로 쿼리를 만들고 쿼리에 setParameter로 parameter를 추가해준다.
강의 출저
'Spring JPA 공부 > 실전! 스프링 부트와 JPA 활용1' 카테고리의 다른 글
변경 감지와 병합(merge) (1) | 2023.01.06 |
---|---|
웹 계층 개발(회원) (0) | 2023.01.06 |
상품 도메인 개발 (0) | 2023.01.04 |
회원 도메인 개발 (0) | 2023.01.04 |
도메인 분석 설계 (0) | 2023.01.03 |