프로젝트 (Java)/예약마켓
[프로젝트] 45. 주문 및 결제 시스템의 도메인과 API 설계
hihyuk
2024. 2. 13. 10:15
핵심 구성 요소
Order
사용자에 의해 생성된 주문을 나타냅니다. 사용자 정보, 주문 상태, 주문 항목 리스트 등을 관리합니다.
@Entity
@Table(name = "orders")
@Getter
@NoArgsConstructor
public class Order extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch= FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id")
private User user;
@Enumerated(EnumType.STRING)
private OrderStatus status;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> orderItems = new ArrayList<>();
@Builder
public Order(User user, OrderStatus status) {
this.user = user;
this.status = status;
}
public void updateStatus(OrderStatus newStatus) {
this.status = newStatus;
}
public void cancel() {
this.status = OrderStatus.CANCEL;
for (OrderItem orderItem : orderItems) {
orderItem.getItem().addStock(orderItem.getCount());
}
}
public void addOrderItem(OrderItem orderItem) {
this.orderItems.add(orderItem);
orderItem.getItem().removeStock(orderItem.getCount()); // 주문 항목 추가 시 재고 감소
if (orderItem.getOrder() != this) {
orderItem.changeOrder(this);
}
}
public void removeOrderItem(OrderItem orderItem) {
this.orderItems.remove(orderItem);
orderItem.getItem().addStock(orderItem.getCount()); // 주문 항목 제거 시 재고 추가
if (orderItem.getOrder() == this) {
orderItem.changeOrder(null);
}
}
}
OrderItem
주문된 개별 상품 정보를 나타냅니다. 상품 정보, 주문 가격, 수량 등을 포함합니다.
@Entity
@Table(name = "order_items")
@Getter
@NoArgsConstructor
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "item_id")
private Item item;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
@Column(name = "order_price")
private int orderPrice;
@Column(name = "count")
private int count;
@Builder
public OrderItem(Long id, Item item, Order order, int orderPrice, int count) {
this.id = id;
this.item = item;
this.order = order;
this.orderPrice = orderPrice;
this.count = count;
}
public void changeOrder(Order order) {
if (this.order != null) {
this.order.getOrderItems().remove(this);
}
this.order = order;
if (order != null) {
order.getOrderItems().add(this);
}
}
}
OrderStatus
주문의 현재 상태를 나타내는 열거형입니다. 주문, 준비, 실패, 취소 등의 상태를 포함합니다.
public enum OrderStatus {
ORDER, PREPARATION, FAIL, CANCEL
}
- Item: 상품의 추상 클래스입니다. 일반 상품과 예약 상품을 관리하는데 사용됩니다.
- GeneralItem / ReservedItem: Item의 구체적인 구현체입니다. 일반 상품과 예약 가능한 상품의 정보를 관리합니다.
API 기능
주문 생성: 사용자가 선택한 상품으로 새 주문을 생성합니다. CreateOrderItemDto를 통해 주문 항목 정보를 받아 처리합니다.
CreateOrderItemDto
@Getter
public class CreateOrderItemDto {
private Long itemId;
private int count;
public OrderItem toEntity(Item item) {
item.removeStock(count); // stock 감소
return OrderItem.builder()
.item(item)
.orderPrice(item.getPrice() * count)
.count(count)
.build();
}
}
주문 조회: 사용자의 모든 주문 혹은 특정 주문을 조회합니다.
OrderDto
@Getter
public class OrderDto {
private Long id;
private Long userId;
private String name;
private OrderStatus orderStatus;
private List<OrderItemDto> orderItems;
private String createdAt;
@Builder
public OrderDto(Long id, Long userId, String name, OrderStatus orderStatus, List<OrderItemDto> orderItems, String createdAt) {
this.id = id;
this.userId = userId;
this.name = name;
this.orderStatus = orderStatus;
this.orderItems = orderItems;
this.createdAt = createdAt;
}
public static OrderDto of (Order order) {
return OrderDto.builder()
.id(order.getId())
.userId(order.getUser().getId())
.name(order.getUser().getName())
.orderStatus(order.getStatus())
.orderItems(order.getOrderItems().stream().map(OrderItemDto::of).collect(toList()))
.createdAt(order.getCreatedAt())
.build();
}
}
OrderItemDto
@Getter
public class OrderItemDto {
private String itemName;
private String itemType;
private int orderPrice;
private int count;
@Builder
public OrderItemDto(String itemName, String itemType, int orderPrice, int count) {
this.itemName = itemName;
this.itemType = itemType;
this.orderPrice = orderPrice;
this.count = count;
}
public static OrderItemDto of (OrderItem orderItem) {
// Item의 실제 타입에 따라 itemType을 설정
String type = orderItem.getItem() instanceof GeneralItem ? "GENERAL" :
orderItem.getItem() instanceof ReservedItem ? "RESERVED" : "UNKNOWN";
return OrderItemDto.builder()
.itemName(orderItem.getItem().getName())
.itemType(type)
.orderPrice(orderItem.getOrderPrice())
.count(orderItem.getCount())
.build();
}
}
- 주문 상태 변경: 주문의 상태를 업데이트합니다. 결제 완료, 주문 취소 등의 처리를 수행합니다.
- 주문 항목 관리: 주문에 포함된 항목을 추가하거나 제거합니다.
이 시스템은 사용자가 웹사이트나 앱을 통해 상품을 주문하고 결제하는 전체 프로세스를 지원합니다. 상품의 재고 관리, 주문의 상태 관리, 사용자와 주문 정보의 연동 등의 기능을 통해 효율적인 주문 및 결제 시스템을 구현할 수 있습니다.
OrderRepository
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByUserId(Long userId);
}
OrderService 클래스는 주문 생성부터 결제, 주문 취소 등의 비즈니스 로직을 처리하는 핵심 서비스로, 데이터베이스와의 상호작용을 관리하며, OrderController는 클라이언트로부터의 HTTP 요청을 받아 서비스 로직을 호출하고 응답을 반환합니다.
OrderController
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@PostMapping
public ResponseEntity<Long> enterPayment(@RequestBody List<CreateOrderItemDto> orderItemDtos) {
Long userId = AuthenticationUtils.getUserIdByToken();
Long orderId = orderService.prepareOrder(userId, orderItemDtos);
return ResponseEntity.ok(orderId);
}
@PostMapping("/pay/{orderId}")
public ResponseEntity<?> completePayment(@PathVariable Long orderId) {
orderService.completeOrder(orderId);
return ResponseEntity.ok().build();
}
@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) {
orderService.cancelOrder(orderId);
return ResponseEntity.ok().build();
}
@DeleteMapping("/{orderId}/items/{orderItemId}")
public ResponseEntity<?> removeOrderItem(@PathVariable Long orderId, @PathVariable Long orderItemId) {
orderService.removeOrderItem(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();
}
}
OrderService
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final ItemRepository itemRepository;
private final UserRepository userRepository;
@Transactional
public Long prepareOrder(Long userId, List<CreateOrderItemDto> orderItemDtos) {
User user = findByUserId(userId);
Order order = Order.builder()
.user(user)
.status(OrderStatus.PREPARATION) // 주문 상태를 준비 상태로 설정
.build();
for (CreateOrderItemDto dto : orderItemDtos) {
Item item = findByItemId(dto.getItemId());
OrderItem orderItem = dto.toEntity(item);
order.addOrderItem(orderItem);
}
orderRepository.save(order);
return order.getId();
}
@Transactional
public void completeOrder(Long orderId) {
Order order = findByOrderId(orderId);
order.processPayment();
orderRepository.saveAndFlush(order);
if (order.getStatus() == OrderStatus.FAIL) {
throw new BadRequestException("결제 요청 실패");
}
}
@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 cancelOrder(Long orderId) {
Order order = findByOrderId(orderId);
order.cancel();
}
@Transactional
public void removeOrderItem(Long orderId, Long orderItemId) {
Order order = findByOrderId(orderId);
OrderItem orderItemToRemove = order.getOrderItems().stream()
.filter(orderItem -> orderItem.getId().equals(orderItemId))
.findFirst()
.orElseThrow(() -> new BadRequestException("주문 목록이 존재하지 않습니다."));
order.removeOrderItem(orderItemToRemove);
}
@Transactional(readOnly = true)
public List<OrderDto> getOrders() {
List<Order> orders = orderRepository.findAll();
return orders.stream().map(OrderDto::of).collect(Collectors.toList());
}
@Transactional
public void deleteOrder(Long orderId) {
Order order = findByOrderId(orderId);
orderRepository.delete(order);
}
private User findByUserId(Long userId) {
return userRepository.findById(userId).orElseThrow(
() -> new BadRequestException("유저 정보를 찾을 수 없습니다.")
);
}
private Order findByOrderId(Long orderId) {
return orderRepository.findById(orderId).orElseThrow(
() -> new BadRequestException("주문 내역이 존재하지 않습니다.")
);
}
private Item findByItemId(Long itemId) {
return itemRepository.findById(itemId).orElseThrow(
() -> new BadRequestException("상품이 존재하지 않습니다.")
);
}
}
이러한 설계는 유연성과 확장성을 고려하여 구현되었으며, 실제 비즈니스 요구사항에 맞게 추가적인 기능이나 변경사항을 쉽게 적용할 수 있습니다.