프로젝트 (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("상품이 존재하지 않습니다.")
        );
    }
}

이러한 설계는 유연성과 확장성을 고려하여 구현되었으며, 실제 비즈니스 요구사항에 맞게 추가적인 기능이나 변경사항을 쉽게 적용할 수 있습니다.