주문 로직을 변경하여 실시간 재고 관리를 위해 WebClient를 사용하여 Redis에 저장된 재고 정보와 연결합니다. 이를 통해 주문 처리 시 재고 수량을 실시간으로 업데이트하고, 주문 취소 시에는 재고를 복원합니다.
주요 변경 사항
- 주문 준비(prepareOrder), 주문 처리(processOrder), 주문 취소(cancelOrder), 주문 항목 제거(removeOrderItem), 주문 삭제(deleteOrder) 등의 로직에 재고 관리 로직을 추가했습니다.
- 주문이 성공적으로 처리될 때(processOrder), 각 주문 항목에 대한 재고 수량을 업데이트합니다.
- 주문 취소(cancelOrder) 또는 주문 항목 제거(removeOrderItem) 시, 해당 항목의 재고를 복원합니다.
- 주문 삭제(deleteOrder) 시, 모든 주문 항목에 대해 재고를 복원합니다.
OrderController
@Slf4j
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@PostMapping
public ResponseEntity<Long> prepareOrder(@RequestBody List<CreateOrderItemDto> orderItemDtos) {
Long userId = AuthenticationUtils.getUserIdByToken();
Long orderId = orderService.prepareOrder(userId, orderItemDtos);
return ResponseEntity.ok().body(orderId);
}
@PostMapping("/pay/{orderId}")
public ResponseEntity<OrderStatus> processOrder(@PathVariable Long orderId) {
Long userId = AuthenticationUtils.getUserIdByToken();
OrderStatus status = orderService.processOrder(userId, orderId);
return ResponseEntity.ok().body(status);
}
@GetMapping("/user")
public ResponseEntity<List<OrderDto>> getUserOrders() {
Long userId = AuthenticationUtils.getUserIdByToken();
List<OrderDto> orders = orderService.getUserOrders(userId);
return ResponseEntity.ok(orders);
}
@PostMapping("/cancel/{orderId}")
public ResponseEntity<?> cancelOrder(@PathVariable Long orderId) {
Long userId = AuthenticationUtils.getUserIdByToken();
orderService.cancelOrder(userId, orderId);
return ResponseEntity.ok().build();
}
@DeleteMapping("/{orderId}/items/{orderItemId}")
public ResponseEntity<?> removeOrderItem(@PathVariable Long orderId, @PathVariable Long orderItemId) {
Long userId = AuthenticationUtils.getUserIdByToken();
orderService.removeOrderItem(userId, orderId, orderItemId);
return ResponseEntity.ok().build();
}
@GetMapping
public ResponseEntity<List<OrderDto>> getOrders() {
List<OrderDto> orders = orderService.getOrders();
return ResponseEntity.ok(orders);
}
@DeleteMapping("/{orderId}")
public ResponseEntity<?> deleteOrder(@PathVariable Long orderId) {
orderService.deleteOrder(orderId);
return ResponseEntity.ok().build();
}
}
WebClient 연결
- OrderService 내에서 WebClient를 사용하여 내부적으로 관리되는 StockService의 API를 호출합니다.
- 재고 수량 업데이트(updateStockQuantity), 재고 수량 복원 로직에 WebClient를 통한 HTTP 요청을 포함시켜, 실시간으로 재고 정보를 업데이트하거나 복원합니다.
OrderService
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final ItemRepository itemRepository;
private final UserRepository userRepository;
private final WebClient webClient;
@Transactional
public Long prepareOrder(Long userId, List<CreateOrderItemDto> orderItemDtos) {
User user = findEntityById(userRepository::findById, userId, "회원");
Order order = createAndSaveOrder(user, orderItemDtos);
return order.getId();
}
private Order createAndSaveOrder(User user, List<CreateOrderItemDto> orderItemDtos) {
Order order = new Order(user, OrderStatus.PREPARATION);
orderItemDtos.forEach(dto -> processOrderItem(dto, order));
return orderRepository.save(order);
}
private void processOrderItem(CreateOrderItemDto dto, Order order) {
Item item = findEntityById(itemRepository::findById, dto.getItemId(), "상품");
validateItemForOrder(item);
OrderItem orderItem = dto.toEntity(item);
order.addOrderItem(orderItem);
}
private void validateItemForOrder(Item item) {
if (item instanceof ReservedItem) {
ReservedItem reservedItem = (ReservedItem) item;
validateReservedItem(reservedItem);
}
}
private void validateReservedItem(ReservedItem reservedItem) {
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(reservedItem.getReservationStart()) || now.isAfter(reservedItem.getReservationEnd())) {
throw new IllegalStateException("예약 가능한 시간이 아닙니다.");
}
}
@Transactional
public OrderStatus processOrder(Long userId, Long orderId) {
findEntityById(userRepository::findById, userId, "회원");
Order order = findEntityById(orderRepository::findById, orderId, "주문");
if (order.getStatus() == OrderStatus.ORDER) {
throw new BadRequestException("이미 주문이 완료되었습니다.");
}
return orderPayAndUpdateStatus(order);
}
private OrderStatus orderPayAndUpdateStatus(Order order) {
// 결제 이탈율 20%
if (Math.random() < 0.2) {
order.updateStatus(OrderStatus.CANCEL);
order.cancel();
return order.getStatus();
}
// 결제 실패율 20%
if (Math.random() < 0.2) {
order.updateStatus(OrderStatus.FAIL);
order.cancel();
return order.getStatus();
}
// 결제 성공
order.updateStatus(OrderStatus.ORDER);
updateOrderItemsStock(order);
Order savedOrder = orderRepository.save(order);
return savedOrder.getStatus();
}
private void updateOrderItemsStock(Order order) {
// 결제 성공 시 각 주문 항목에 대해 재고 수량 업데이트
order.getOrderItems().forEach(orderItem -> {
int newStockQuantity = orderItem.getItem().getStockQuantity();
updateStockQuantity(orderItem.getItem().getId(), newStockQuantity)
.subscribe(result -> log.info(result),
error -> log.error("Error updating stock: ", error));
});
}
@Transactional
public void cancelOrder(Long userId, Long orderId) {
Order order = findEntityById(orderRepository::findById, orderId, "주문");
validateUser(order, userId);
if (order.getStatus() != OrderStatus.PREPARATION) {
throw new BadRequestException("주문 상태가 준비 중이 아니어서 변경할 수 없습니다.");
}
order.updateStatus(OrderStatus.CANCEL);
order.cancel();
orderRepository.save(order);
}
@Transactional
public void removeOrderItem(Long userId, Long orderId, Long orderItemId) {
Order order = findEntityById(orderRepository::findById, orderId, "주문");
validateUser(order, userId);
if (order.getStatus() != OrderStatus.PREPARATION) {
throw new BadRequestException("주문 상태가 준비 중이 아니어서 변경할 수 없습니다.");
}
OrderItem orderItem = findOrderItemById(order, orderItemId);
order.removeOrderItem(orderItem);
orderRepository.save(order);
}
private OrderItem findOrderItemById(Order order, Long orderItemId) {
return order.getOrderItems().stream()
.filter(orderItem -> orderItem.getId().equals(orderItemId))
.findFirst()
.orElseThrow(() -> new BadRequestException("주문 목록이 존재하지 않습니다."));
}
@Transactional(readOnly = true)
public List<OrderDto> getOrders() {
List<Order> orders = orderRepository.findAllWithItems();
return orders.stream().map(OrderDto::of).collect(Collectors.toList());
}
@Transactional(readOnly = true)
public List<OrderDto> getUserOrders(Long userId) {
List<Order> orders = orderRepository.findByUserId(userId);
return orders.stream()
.map(OrderDto::of)
.collect(Collectors.toList());
}
@Transactional
public void deleteOrder(Long orderId) {
Order order = findEntityById(orderRepository::findById, orderId, "주문");
// 주문 삭제 시 재고 복원 로직
order.getOrderItems().forEach(orderItem -> {
int restoredQuantity = orderItem.getItem().getStockQuantity() + orderItem.getCount();
updateStockQuantity(orderItem.getItem().getId(), restoredQuantity)
.subscribe(result -> log.info(result),
error -> log.error("Error restoring stock: ", error));
});
orderRepository.delete(order);
}
private <T> T findEntityById(Function<Long, Optional<T>> finder, Long id, String entityName) {
return finder.apply(id).orElseThrow(() -> new BadRequestException(entityName + " 정보를 찾을 수 없습니다."));
}
private void validateUser(Order order, Long userId) {
if (!order.getUser().getId().equals(userId)) {
throw new BadRequestException("주문 취소 권한이 없습니다.");
}
}
public Mono<String> updateStockQuantity(Long itemId, int stockQuantity) {
return webClient.put()
.uri(buildStockUpdateUri(itemId, stockQuantity))
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.just("Error updating stock quantity: " + e.getMessage()));
}
private String buildStockUpdateUri(Long itemId, int stockQuantity) {
return UriComponentsBuilder.fromHttpUrl("http://localhost:8085")
.path("/api/internal/stocks/{itemId}")
.queryParam("stockQuantity", stockQuantity)
.buildAndExpand(itemId)
.toUriString();
}
}
재고 관리 서비스 (StockService)
- StockService는 Redis에 저장된 재고 정보를 관리합니다. 재고 정보의 저장, 업데이트, 삭제 등을 담당합니다.
- InternalStockController를 통해 재고 수량 업데이트 API를 제공합니다. 이 API는 OrderService에서 WebClient를 통해 호출됩니다.
InternalStockController
@RestController
@RequestMapping("/api/internal/stocks")
@RequiredArgsConstructor
public class InternalStockController {
private final StockService stockService;
@PutMapping("/{itemId}")
public ResponseEntity<?> updateStockQuantity(@PathVariable Long itemId, @RequestParam int stockQuantity) {
stockService.updateStockQuantity(itemId, stockQuantity);
return ResponseEntity.ok().build();
}
}
주요 구현 내용
- 주문 로직 변경: 주문 처리 시 재고 관리 로직을 포함하여, 주문에 따른 재고 수량 변경을 실시간으로 반영합니다.
- WebClient 사용: 내부 재고 관리 서비스의 API를 호출하여 재고 수량을 업데이트하거나 복원합니다.
- 실시간 재고 관리: Redis를 사용한 실시간 재고 관리를 통해, 주문 시스템과 재고 시스템 간의 일관성을 유지합니다.
이 변경을 통해 주문 시스템은 실시간으로 재고 상태를 반영할 수 있으며, 사용자는 정확한 재고 정보를 기반으로 주문을 진행할 수 있게 됩니다. 또한, 재고 관리의 신뢰성 및 정확성이 향상됩니다.
'프로젝트 (Java) > 예약마켓' 카테고리의 다른 글
[프로젝트] 56. 실시간 재고 관리 서비스 리팩토링 (0) | 2024.02.28 |
---|---|
[프로젝트] 55. 프로젝트 세부 사항 정리 및 문서화 (0) | 2024.02.20 |
[프로젝트] 53. Redis를 활용한 실시간 재고 관리 서비스 (0) | 2024.02.20 |
[프로젝트] 52. Docker 활용 예약 시간 재고 확인 자동화 툴 구축 (0) | 2024.02.20 |
[프로젝트] 51. Docker 활용 주문 API 자동화 툴 구축 (1) | 2024.02.20 |