6/13 ~ 6/27
DB 설계, OOP적 설계
Github
이번에는 커머스 플랫폼의 주문 플로우를 비슷하게 구현할 것입니다.
주문 완료
상품 내역 조회
상품 상세 조회
상품 선택 -> 장바구니 담기 -> 주문 완료
이 부분은 주문을 하기 위한 플로우이며, 주문 완료 API를 구현시 앞 프로세스는 완료되었다는 전제로 작성해주세요.
가상의 물류 창고에서 제품을 보관하고 소비자에게 상품을 판매합니다.
하나의 상품은 여러 제품을 포함할 수 있습니다.
제품은 옵션이 각각 다른 상품으로 생각해주시면 됩니다.
(도메인 지식을 배우는 과정이 아니라 하나로 합치겠습니다.)
제품의 재고가 관리되어야 합니다.
결제에 대해서는 신경쓰지 않습니다.
객체 지향이란 무엇인지에 대해서 고민해보고 객체 지향적으로 코드 구조를 설계해주세요.
GitHub Repository는 commerce-server를 Fork 해주세요.
장바구니 데이터는 서버에서 저장하지 않습니다.
도메인 별 구조는 확실하게 나눠주셔야 합니다.
도메인 사이 양방향 참조가 일어나면 안됩니다.
요구사항을 구현하기 위한 DB 설계는 본인이 직접 해야 합니다.
제출 결과물
1) 코드를 작성한 GitHub Repository 주소
2) ERD와 클래스 다이어그램이 들어간 발표자료
주의사항!
요구사항에 적히지 않은 문제에 대해서는 스스로 정의하고 분석을 진행해주세요.

제약사항
제품 테이블 내 가격과 재고만 변동 가능
상품 - 제품 테이블에서 제품, 상품 ID 변동 불가
상품은 제품 테이블에 의해 가격만 변동 가능
지금까지의 커머스 플랫폼에 대한 경험 중, 상품이 삭제되는 경우는 있었어도 다른 상품으로 변경되는 경우는 없었기 때문에 제약사항을 정의했다.


초기 주문 데이터 생성
주문 상품 확인
상품에 대한 제품 확인
제품 재고 확인
상품별 가격 계산
재고 감소
주문 상품 저장
총 금액 계산
주문 상태 추가
@Override
@Transactional
public OrderResponse createOrder(OrderRequest orderRequest) {
// 주문 초기화
Order order = Order.builder()
.contact(orderRequest.getContact())
.address(orderRequest.getAddress())
.build();
orderRepository.save(order);
List<OrderItem> orderItemList = new ArrayList<>();
for (OrderItemRequest orderItemRequest : orderRequest.getOrderItemList()) {
// 주문 항목에 포함된 상품이 존재하는지 확인
Item item = itemRepository.findById(orderItemRequest.getItemId())
.orElseThrow(() -> new BusinessException(HttpResponse.Fail.NOT_FOUND_ITEM));
// 상품에 연결된 제품 목록 가져옴
List<ItemProduct> itemProducts = itemProductRepository.findByItemId(item.getId());
for (ItemProduct itemProduct : itemProducts) {
Product product = productRepository.findById(itemProduct.getProduct().getId())
.orElseThrow(() -> new BusinessException(HttpResponse.Fail.NOT_FOUND_PRODUCT));
// 필요한 수량만큼 제품의 재고가 있는지 확인
Long requiredQuantity = itemProduct.getQuantity() * orderItemRequest.getQuantity();
if (product.getStock() < requiredQuantity) { // 재고 < 필요수량
throw new BusinessException(HttpResponse.Fail.OUT_OF_ITEM_STOCK);
}
// // (product)제품가 * (itemProduct)수량 = (item)상품가와 일치하는지 검증
// BigDecimal itemCost = product.getProductPrice().multiply(BigDecimal.valueOf(itemProduct.getQuantity()));
// if (itemCost.compareTo(item.getItemPrice()) != 0) {
// throw new BusinessException(HttpResponse.Fail.PRICE_MISMATCH);
// }
// 필요한 수량만큼 재고 감소시키고 저장
product.setStock(product.getStock() - requiredQuantity);
productRepository.save(product);
}
// 주문 상품 저장
OrderItem orderItem = OrderItem.builder()
.order(order)
.item(item)
.quantity(orderItemRequest.getQuantity())
.itemPrice(item.getItemPrice())
.itemName(item.getName())
.build();
orderItemRepository.save(orderItem);
orderItemList.add(orderItem);
}
// 총 금액 계산
BigDecimal totalPrice = getTotalPrice(orderItemList);
order.setTotalPrice(totalPrice);
orderRepository.save(order);
// 주문 상태 변경
order.setStatus(DeliveryStatus.ACCEPTED);
// 주문 응답 생성
List<OrderItemResponse> orderItemResponses = createOrderItemResponses(orderItemList);
return OrderResponse.of(order, orderItemResponses, totalPrice);
}
@Override
public BigDecimal getTotalPrice(List<OrderItem> orderItemList) {
BigDecimal totalPrice = BigDecimal.ZERO;
for (OrderItem orderItem : orderItemList) {
BigDecimal total = orderItem.getItemPrice().multiply(BigDecimal.valueOf(orderItem.getQuantity()));
totalPrice = totalPrice.add(total);
}
return totalPrice;
}
// 주문 항목 응답 객체들을 생성하여 리스트로 반환
@Override
public List<OrderItemResponse> createOrderItemResponses(List<OrderItem> orderItemList) {
List<OrderItemResponse> orderItemResponses = new ArrayList<>();
for (OrderItem orderItem : orderItemList) {
OrderItemResponse orderItemResponse = OrderItemResponse.of(orderItem);
orderItemResponses.add(orderItemResponse);
}
return orderItemResponses;
}
의존 관계
createOrder 메서드는 getTotalPrice와 createOrderItemResponses 메서드를 호출하여 필요한 기능을 수행한다.
getTotalPrice 메서드는 createOrder 메서드의 주문 총 금액 계산에 사용한다.
createOrderItemResponses 메서드는 createOrder 메서드의 주문 응답 생성에 사용한다.
POST localhost:8090/v1/orders
REQUEST
{
"contact": 1012345678,
"address": "엔터팰리스3차",
"orderItemList": [
{
"itemId": 2,
"quantity": 2
},
{
"itemId": 3,
"quantity": 2
}
]
}
RESPONSE
{
"id": 6,
"totalPrice": 26000.000,
"address": "엔터팰리스3차",
"contact": 1012345678,
"status": "ACCEPTED",
"orderItemList": [
{
"orderItemId": 10,
"itemId": 2,
"quantity": 2,
"itemPrice": 9000.000,
"itemName": "아이시스 6개입"
},
{
"orderItemId": 11,
"itemId": 3,
"quantity": 2,
"itemPrice": 4000.000,
"itemName": "칸쵸 4개입"
}
]
}
상품 정보를 단순 출력하는 기능
GET localhost:8090/v1/items
RESPONSE
[
{
"id": 1,
"name": "열라면 5개입",
"itemPrice": 5000.000
},
{
"id": 2,
"name": "아이시스 6개입",
"itemPrice": 9000.000
},
{
"id": 3,
"name": "칸쵸 4개입",
"itemPrice": 4000.000
},
{
"id": 4,
"name": "김치볶음밥 8개입",
"itemPrice": 16000.000
},
{
"id": 5,
"name": "김치볶음밥, 잡채볶음밥 8개입",
"itemPrice": 16000.000
},
{
"id": 6,
"name": "아이폰 16",
"itemPrice": 1000000.000
}
]
상품 한가지의 정보를 단순 출력하는 기능
GET localhost:8090/v1/items/3
RESPONSE
{
"id": 1,
"name": "열라면 5개입",
"itemPrice": 5000.000
}
오류 해결
stock의 status 추가(품절, 품절임박, ..)
배달 플랫폼, 커머스 플랫폼을 도메인 모델 패턴으로 변경해보기
setter 지양(객체의 일관성 유지하기 어려움, 값을 변경한 의도를 파악하기 힘듬)
생성자를 오버로딩, Builder 패턴 사용, 정적 팩토리 메소드 사용 권장