반응형
package com.zerobase.cms.order.application;
import com.zerobase.cms.order.client.UserClient;
import com.zerobase.cms.order.client.user.ChangeBalanceForm;
import com.zerobase.cms.order.client.user.CustomerDto;
import com.zerobase.cms.order.domain.model.ProductItem;
import com.zerobase.cms.order.domain.redis.Cart;
import com.zerobase.cms.order.exception.CustomException;
import com.zerobase.cms.order.exception.ErrorCode;
import com.zerobase.cms.order.service.ProductItemService;
import java.util.stream.IntStream;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class OrderApplication {
private final CartApplication cartApplication;
private final UserClient userClient;
private final ProductItemService productItemService;
// 결제를 위해 필요한 것
// 1. 물건들이 전부 주문 가능한 상태인지 확인
// 2. 가격 변동이 있었는지에 대한 확인
// 3. 고객의 돈이 충분한지 확인
// 4. 결제 & 상품의 재고 관리
@Transactional
public void order(String token, Cart cart) {
// 1. 주문 시 기존 카트 버림
// 2. 선택 주문: 내가 사지 않은 아이템을 살려야 함.
Cart orderCart = cartApplication.refreshCart(cart);
if (orderCart.getMessages().size() > 0) {
throw new CustomException(ErrorCode.ORDER_FAIL_CHECK_CART);
}
CustomerDto customerDto = userClient.getCustomerInfo(token).getBody();
int totalPrice = getTotalPrice(cart);
if (customerDto.getBalance() < totalPrice) {
throw new CustomException(ErrorCode.ORDER_FAIL_NO_MONEY);
}
// 롤백 계획에 대해 생각해야 함.
userClient.changeBalance(token,
ChangeBalanceForm.builder()
.from("USER")
.message("Order")
.money(-totalPrice)
.build());
for (Cart.Product product : orderCart.getProducts()) {
for (Cart.ProductItem cartItem : product.getItems()) {
ProductItem productItem = productItemService.getProductItem(cartItem.getId());
productItem.setCount(productItem.getCount() - cartItem.getCount());
}
}
}
private Integer getTotalPrice(Cart cart) {
return cart.getProducts().stream().flatMapToInt(product ->
product.getItems().stream().flatMapToInt(productItem ->
IntStream.of(productItem.getPrice() * productItem.getCount()))).sum();
}
}
주문 처리 애플리케이션 (OrderApplication)
이 코드는 온라인 쇼핑몰에서 실제 주문을 처리하는 애플리케이션 서비스(OrderApplication)입니다. 장바구니에 담긴 상품을 결제하고, 재고를 관리하며, 사용자 잔액을 업데이트하는 기능을 수행합니다.
1. 클래스 구조 및 의존성
- @Service: Spring의 어노테이션으로, 해당 클래스가 Spring Bean으로 관리되어야 함을 나타냅니다. 즉, Spring 컨테이너가 이 클래스의 인스턴스를 생성하고 관리합니다.
- @RequiredArgsConstructor: Lombok 라이브러리의 어노테이션으로, final 필드나 @NonNull 어노테이션이 붙은 필드에 대한 생성자를 자동으로 생성해줍니다. 여기서는 cartApplication, userClient, productItemService를 주입받기 위한 생성자가 만들어집니다.
- CartApplication: 장바구니 관련 기능을 제공하는 서비스. 장바구니 정보 갱신 등에 사용됩니다.
- UserClient: 사용자 정보 및 잔액 관련 기능을 제공하는 클라이언트. Spring Cloud OpenFeign을 사용하여 USER 마이크로서비스와 통신합니다.
- ProductItemService: 상품 아이템 관련 기능을 제공하는 서비스. 상품 아이템 정보 조회 및 재고 관리에 사용됩니다.
- @Transactional: Spring Framework의 어노테이션으로, 해당 메서드가 트랜잭션으로 처리되어야 함을 나타냅니다. 트랜잭션은 데이터베이스의 상태를 변화시키는 작업의 단위이며, ACID (Atomicity, Consistency, Isolation, Durability) 속성을 보장합니다.
2. 메서드별 상세 설명
2.1. order(String token, Cart cart)
- 기능: 주문을 처리합니다.
- 주문 시 기존 카트 갱신:
- cartApplication.refreshCart(cart)를 호출하여 장바구니 정보를 갱신합니다.
- refreshCart 메서드는 상품 정보, 가격, 수량 변경 여부를 확인하고, 변경된 사항이 있으면 장바구니 정보를 업데이트합니다.
- 카트에 오류 메시지가 있는지 확인:
- if (orderCart.getMessages().size() > 0)를 사용하여 카트에 오류 메시지가 있는지 확인합니다.
- 만약 오류 메시지가 있으면 (orderCart.getMessages().size() > 0), CustomException을 발생시켜 ErrorCode.ORDER_FAIL_CHECK_CART 에러를 반환합니다.
- 이는 장바구니에 담긴 상품 정보가 변경되어 주문을 진행할 수 없는 경우를 처리하기 위함입니다.
- 사용자 정보 가져오기:
- userClient.getCustomerInfo(token).getBody()를 호출하여 사용자 정보를 가져옵니다.
- token은 사용자를 인증하기 위한 정보이며, UserClient를 통해 USER 마이크로서비스에 요청하여 사용자 정보를 가져옵니다.
- getBody() 메서드는 응답 body에서 실제 CustomerDto 객체를 추출합니다.
- 총 결제 금액 계산:
- getTotalPrice(cart)를 호출하여 총 결제 금액을 계산합니다.
- getTotalPrice 메서드는 장바구니에 담긴 상품들의 가격과 수량을 곱하여 총 결제 금액을 계산합니다.
- 사용자 잔액이 충분한지 확인:
- if (customerDto.getBalance() < totalPrice)를 사용하여 사용자 잔액이 충분한지 확인합니다.
- 만약 잔액이 부족하면 (customerDto.getBalance() < totalPrice), CustomException을 발생시켜 ErrorCode.ORDER_FAIL_NO_MONEY 에러를 반환합니다.
- 사용자 잔액 차감:
- userClient.changeBalance(...)를 호출하여 사용자 잔액을 차감합니다.
- ChangeBalanceForm 객체를 생성하여 잔액 변경에 필요한 정보를 담아 USER 마이크로서비스에 요청합니다.
- money(-totalPrice)는 총 결제 금액만큼 잔액을 차감하는 것을 의미합니다.
- from("USER")는 잔액 변경 요청을 보낸 주체가 USER 마이크로서비스임을 나타냅니다.
- message("Order")는 잔액 변경 이유를 나타냅니다.
- 상품 재고 감소:
- for (Cart.Product product : orderCart.getProducts())를 사용하여 장바구니에 담긴 상품 목록을 순회합니다.
- for (Cart.ProductItem cartItem : product.getItems())를 사용하여 각 상품에 대한 아이템 목록을 순회합니다.
- ProductItem productItem = productItemService.getProductItem(cartItem.getId());를 호출하여 상품 아이템 정보를 가져옵니다.
- productItem.setCount(productItem.getCount() - cartItem.getCount());를 사용하여 상품 아이템의 재고를 감소시킵니다.
- 주문 시 기존 카트 갱신:
2.2. getTotalPrice(Cart cart)
- 기능: 장바구니에 담긴 상품들의 총 가격을 계산합니다.
- cart.getProducts().stream():
- cart에서 상품 목록을 가져와 Stream으로 변환합니다.
- Stream은 컬렉션 데이터를 효율적으로 처리하기 위한 기능입니다.
- .flatMapToInt(product -> product.getItems().stream().flatMapToInt(productItem -> IntStream.of(productItem.getPrice() * productItem.getCount()))):
- 각 상품에 대해 아이템 목록을 Stream으로 변환하고, 각 아이템의 가격과 수량을 곱한 값을 IntStream으로 변환합니다.
- flatMapToInt는 중첩된 Stream을 하나의 IntStream으로 평탄화하는 역할을 합니다.
- IntStream은 int 타입의 데이터를 효율적으로 처리하기 위한 Stream입니다.
- .sum():
- IntStream에 있는 모든 값들을 더하여 총 가격을 계산합니다.
- sum()은 IntStream의 모든 요소의 합을 반환합니다.
- cart.getProducts().stream():
3. 핵심 개념 및 추가 설명
- Stream API: Java 8부터 도입된 Stream API는 컬렉션 데이터를 효율적으로 처리하기 위한 기능입니다. stream(), map(), filter(), collect() 등의 메서드를 사용하여 데이터를 변환하고 수집할 수 있습니다.
- flatMapToInt: flatMapToInt는 스트림의 각 요소를 IntStream으로 변환한 후, 모든 IntStream을 하나의 IntStream으로 평탄화합니다. 즉, 중첩된 스트림을 하나의 스트림으로 합치는 역할을 합니다.
- IntStream: IntStream은 int 타입의 데이터를 효율적으로 처리하기 위한 Stream입니다. IntStream은 mapToInt, flatMapToInt, sum, average 등의 메서드를 제공합니다.
- 트랜잭션 (Transaction): 트랜잭션은 데이터베이스의 상태를 변화시키는 작업의 단위이며, ACID (Atomicity, Consistency, Isolation, Durability) 속성을 보장합니다.
- Atomicity (원자성): 트랜잭션 내의 모든 작업은 완전히 성공하거나 완전히 실패해야 합니다.
- Consistency (일관성): 트랜잭션이 완료된 후에도 데이터베이스는 항상 일관된 상태를 유지해야 합니다.
- Isolation (고립성): 동시에 실행되는 트랜잭션은 서로에게 영향을 주지 않아야 합니다.
- Durability (지속성): 트랜잭션이 성공적으로 완료되면, 그 결과는 영구적으로 데이터베이스에 저장되어야 합니다.
- Spring Cloud OpenFeign: Spring Cloud OpenFeign은 선언적이고 간편하게 REST API 클라이언트를 생성할 수 있는 라이브러리입니다. OpenFeign을 사용하면 HTTP 클라이언트 코드를 직접 작성하지 않고도 인터페이스를 통해 REST API를 호출할 수 있습니다.
4. 고려할 점
- 롤백 (Rollback): order 메서드에 @Transactional 어노테이션이 적용되어 있으므로, 메서드 실행 중 예외가 발생하면 트랜잭션이 롤백됩니다. 롤백은 트랜잭션 내에서 수행된 모든 변경 사항을 취소하고, 데이터베이스를 트랜잭션 시작 이전의 상태로 되돌리는 것을 의미합니다.
- 재고 관리: 상품 재고를 감소시키는 로직은 경쟁 조건 (Race Condition)이 발생할 수 있습니다. 여러 사용자가 동시에 동일한 상품을 주문하는 경우, 재고가 음수가 될 수 있습니다. 이를 방지하기 위해 데이터베이스 레벨에서 Lock을 사용하거나, Redis의 분산 Lock을 사용하여 동시성 문제를 해결할 수 있습니다.
- 결제 시스템 연동: 실제 결제 시스템과 연동하여 결제를 처리해야 합니다. 결제 시스템 연동 시에는 다양한 예외 상황 (결제 실패, 결제 취소 등)에 대한 처리가 필요합니다.
- 비동기 처리: 상품 재고 감소 및 사용자 잔액 변경은 시간이 오래 걸릴 수 있는 작업입니다. 이러한 작업을 비동기적으로 처리하여 응답 시간을 단축할 수 있습니다.
반응형