SpringBoot와 JPA, Thymeleaf를 사용하여 간단한 쇼핑몰을 구현하려 합니다. 프로젝트 생성이나 Gradle 빌드는 깃허브 BincoShop을 참고해주세요! 포스팅은 도메인&테이블 설계 -> 엔티티 개발 -> 회원 서비스 -> 상품 서비스 -> 주문 서비스 순으로 진행됩니다. 포스팅의 잘못된 부분은 언제든 댓글로 남겨주시면 수정하겠습니다😀
개요
주문 로직은 사용자가 회원, 상품, 수량을 선택하고 주문 시에 로직이 진행됩니다. 그럼 Service 단에서 비즈니스 로직을 어떻게 처리하는지 알아볼까요?
Service📘
@Service
@Transactional
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final MemberRepository memberRepository;
private final ItemRepository itemRepository;
// 주문
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();
}
// 취소
public void cancelOrder(Long orderId) {
// 엔티티 조회
Order order = orderRepository.findOne(orderId);
// 주문 취소
order.cancel();
}
}
주문은 회원, 아이템, 주문 관련 기능이 필요하기 때문에 각각의 Repository를 주입받았습니다. 요구사항 분석에서 봤다시피, 회원과 상품, 수량을 입력하기 때문에 각각의 값을 이용해 Order Entity를 생성해야 합니다.
처음으로 Member, Item 엔티티를 조회하는 로직이 들어갔습니다. Member는 주문을 생성할 때, Item은 주문 상품을 생성할 때 필요하기 때문입니다. 그리고 배송정보는 따로 입력받지 않고 회원가입 시에 입력했던 주소를 넣기 때문에 Delivery Entity를 생성하고, 조회한 Member Entity에서 해당 멤버의 주소를 getAddres를 통해 배송지를 설정합니다.
이후에는 주문상품을 생성해야 합니다. 조회한 Item Entity와 그 아이템의 가격, 입력받은 개수를 이용해 주문 상품을 생성하였습니다. createOrderItem 메서드는 지난 포스팅에서 작성하였습니다.
마지막으로 주문을 생성해야 합니다. 이 부분도 위와 마찬가지로 지난 포스팅에서 만든 생성 메서드를 이용하여 작성하였습니다. 매개변수로 조회한 Member Entity, 배송정보, 이전에 생성한 주문 상품을 넣어서 새로운 주문을 생성합니다.
// 주문 저장
orderRepository.save(order);
이후에 OrderRepository에 만들어 놓은 save() 메서드를 호출하면 끝입니다. 여기서 한가지 의문점이 생깁니다. 이전에 만든 주문 상품과 배달정보는 어떻게 저장되었을까요? 생성 메서드만 있을 뿐, 따로 업데이트 쿼리 같은 것을 작성하지 않았는데 말이죠.
그것은 각각의 필드들이 Cascade를 입혀놨기 때문입니다. 그렇기에 주문을 저장했을 때, 주문 로직을 타는 과정에 있는 필드들 중에서 Cascade가 입힌 필드들도 다 같이 저장되기 때문입니다.
// 취소
public void cancelOrder(Long orderId) {
// 엔티티 조회
Order order = orderRepository.findOne(orderId);
// 주문 취소
order.cancel();
}
취소 로직은 정말 간단하게 구현됩니다. 취소할 주문 엔티티를 가져와서 cancel 메서드만 기입하면 끝이 나기 때문입니다.
지난번에 Entity에 기입한 cancel 메서드를 통해 해당 상품은 취소가 되고, 반복 루프를 돌면서 취소된 아이템들의 재고를 복원시켜주는 로직을 만들었었습니다. 이런식의 개발 방식을 도메인 모델 패턴이라고 합니다.
📌참고 : 주문 서비스의 주문과 주문 취소 메서드를 보면 비즈니스 로직 대부분이 엔티티에 있습니다. 서비스 계층은 단순히 엔티티에 필요한 요청을 위임하는 역할만 합니다. 이처럼 엔티티가 비즈니스 로직을 가지고 객체 지향의 특성을 적극 활용하는 것을 도메인 모델 패턴이라 합니다.
반대로 엔티티에는 비즈니스 로직이 거의 없고 서비스 계층에서 대부분의 비즈니스 로직을 처리하는 것을 트랜잭션 스크립트 패턴이라고 합니다.
TDD📙
@SpringBootTest
@Transactional
class OrderServiceTest {
@Autowired
EntityManager em;
@Autowired
OrderService orderService;
@Autowired
OrderRepository orderRepository;
private Book CreateBook(String name, int price, int stockQuantity) {
Book 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;
}
}
테스트 코드에 필요한 기본 설정을 했습니다. 각각의 어노테이션은 회원 서비스 테스트에서 설명하였으므로 넘어가겠습니다. CreateBook과 CreateMember는 반복적으로 테스트 케이스를 만들어야 하기 때문에 static으로 빼놓았습니다.
1. 상품 주문 테스트
@Test
@DisplayName("상품 주문")
public void order() throws Exception {
// given
Member member = CreateMember();
Book book = CreateBook("JPA BOOK", 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("주문 수량 만큼 재고가 줄어야 한다", 8, book.getStockQuantity());
}
given 단계에서는 static 메서드를 이용하여 회원과 아이템을 각각 만들어줍니다. 그리고 임의의 주문 수량도 지정해줍니다.
when 단계에서는 orderService의 order() 메서드를 이용하여 해당 주문의 id 값을 반환받습니다. order() 메서드에는 회원의 id와 아이템의 id, 주문수량을 인자 값으로 넣어줍니다.
then 단계에서는 orderRepository의 findOne() 메서드를 이용하여 주문 Entity를 반환받습니다. 이제 assertEquals를 이용하여 주문 상태, 상품 종류 수, 총 가격, 재고 일치 여부를 확인합니다.
2. 주문 취소 테스트
@Test
@DisplayName("주문 취소")
public void cancel() throws Exception {
// given
Member member = CreateMember();
Book 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());
}
given 단계에서는 맴버와 아이템(Book)을 static 메서드를 이용해 생성합니다. 그리고 주문 수량을 임의로 저장한 후에 orderService의 order() 메서드를 호출합니다. 이때 인자 값으로 회원의 id, 아이템의 id, 주문 수량을 넣어줍니다. 그러면 해당 주문의 id 값이 반환됩니다.
when 단계에서는 해당 주문의 id 값을 이용하여 orderService에 있는 cancelOrder() 메서드를 호출합니다.
then 단계에서는 orderRepository에서 해당 주문을 찾습니다. 그리고 assertEquals를 이용해서 해당 주문의 상태와 재고가 원복되는지 체크합니다.
👨👩👦👦 오픈채팅방 운영
취업을 준비하는 예비 개발자분들을 위한 질문&답변할 수 있는 공간을 만들었습니다. 취업과 이직을 하기 위해서 어떤 걸 중점적으로 준비해야 하는지부터 포트폴리오&이력서 작성법 등 다양한 질문들을 받고 답변을 드립니다. 참여하셔서 다양한 정보 얻고 가시면 좋을 것 같네요😁
참여코드 : 456456
https://open.kakao.com/o/gVHZP8dg
👨💻 전자책 출간
아울러 제가 🌟비전공자에서 2년만에 보안 전문 중견기업으로 이직 한 방법들을 정리한 전자책을 출간 하게 되었습니다. 어떤 걸 공부해야 하는지, 이직을 위해서 무엇을 준비해야 하는지, 제가 받았던 기술 면접 리스트 등 다양한 목차로 구성되어 있습니다. 또한, 구매 시 1:1 채팅을 이용하여 포트폴리오 첨삭을 도와드리고 있습니다. 🐕전자책으로 얻은 모든 수익은 유기견 센터 '팅*벨 입양센터'에 후원될 예정입니다. 관심 있으신 분들은 아래 링크를 참고해주세요😁
마치며
지금까지 주문 서비스의 기본 비즈니스 로직을 구현하고, 해당 로직들이 잘 수행되는지 테스트 케이스를 작성했습니다. 도메인 모델 개발 패턴이 아직은 익숙하지는 않지만 확실히 코드상에서 불필요한 코드들이 많이 줄어드는 것 같아서 앞으로 JPA 공부를 더 해야겠다는 생각이 드네요😄
관련 포스팅
Reference
'JPA' 카테고리의 다른 글
Spring Data JPA와 기존 JPA의 차이점 및 사용법 (0) | 2022.10.13 |
---|---|
SpringBoot JPA 쇼핑몰 주문 검색 (0) | 2022.09.19 |
SpringBoot JPA 쇼핑몰 주문 서비스 I (0) | 2022.09.15 |
SpringBoot JPA 쇼핑몰 상품 서비스 개발 (0) | 2022.09.14 |
SpringBoot JPA 쇼핑몰 회원 서비스 개발 (1) | 2022.09.14 |