모든 쇼핑몰에서 사용자 주문, 결제, 배송 과정은 거의 모두 동일 합니다. 이러한 트랜잭션의 각 단계를 효과적으로 관리하고 추적하는 것은 사용자 경험을 향상시키고 운영 효율성을 높이는데 도움을 줍니다. 이번 포스팅에서는 몽고디비를 활용하여 주문 아이디 기준으로 스텝별 상태를 저장하고 조회하는 방법을 소개합니다.
시퀀스
도커 컴포즈 설정
몽고디비 도커 컴포즈를 통해 구성합니다.
version: '3.8'
services:
# MongoDB configuration
mongodb:
image: mongo:latest
container_name: mongodb_container
ports:
- "27017:27017"
volumes:
- mongodb_data:/data/db
environment:
MONGO_INITDB_ROOT_USERNAME: rootuser
MONGO_INITDB_ROOT_PASSWORD: rootpassword
volumes:
mongodb_data:
스프링 부트 설정
application.yml 파일에는 몽고디비와의 연결 정보를 포함하여 스프링 부트 애플리케이션 설정을 정의합니다.
spring:
data:
mongodb:
database: shoppingmall
port: 27017
host: localhost
username: rootuser
password: rootpassword
uri: mongodb://${spring.data.mongodb.username}:${spring.data.mongodb.password}@localhost:${spring.data.mongodb.port}
주요 패키지 및 클래스 설명
common 패키지
Step 열거형은 주문 프로세스의 각 단계를 나타냅니다.
Step.kt
enum class Step {
TRY_ORDER,
DONE_ORDER,
TRY_PAYMENT,
DONE_PAYMENT,
TRY_DELIVERY,
DONE_DELIVERY,
TRY_CANCELED,
DONE_CANCELED,
FAIL,
}
domain 패키지
Order와 OrderStep 클래스는 주문 정보와 주문 프로세스의 각 단계를 나타냅니다.
data class Order(
@Id
val orderId: String,
@Indexed
val userId: String,
val steps: List<OrderStep>? = null,
)
data class OrderStep(
val step: Step,
val data: String,
val createdAt: LocalDateTime = LocalDateTime.now(),
val updatedAt: LocalDateTime = LocalDateTime.now()
)
interface OrderRepository: MongoRepository<Order, String> {
fun findByOrderIdAndUserId(orderId: String, userId: String): Optional<Order>
fun findByOrderId(orderId: String): Optional<Order>
}
서비스 패키지
OrderService 클래스는 주문 생성, 결제, 배송 등의 비즈니스 로직을 담당합니다.
@Service
class OrderService(
val orderRepository: OrderRepository,
val mongoTemplate: MongoTemplate
) {
fun createOrder(request: OrderRequest): OrderResponse {
val orderId = UUID.randomUUID().toString()
val initialSteps = listOf(
OrderStep(step = Step.TRY_ORDER, data = "orderId: $orderId, request: $request")
)
val order = Order(orderId = orderId, userId = request.userId, steps = initialSteps)
processOrderStep(order, Step.DONE_ORDER, "orderId: $orderId, request: $request")
return OrderResponse(orderId, request.userId, Step.DONE_ORDER)
}
fun addPayment(request: PaymentRequest): PaymentResponse {
val order = orderRepository.findByOrderIdAndUserId(request.orderId, request.userId)
.orElseThrow { NoSuchElementException("Order not found with orderId: ${request.orderId} and userId: ${request.userId}") }
val paymentId = UUID.randomUUID().toString()
val orderAfterTryPayment = processOrderStep(order, Step.TRY_PAYMENT, "paymentId: $paymentId, request: $request")
processOrderStep(orderAfterTryPayment, Step.DONE_PAYMENT, "paymentId: $paymentId, request: $request")
return PaymentResponse(request.orderId, request.userId, paymentId, Step.DONE_PAYMENT)
}
fun addDelivery(request: DeliveryRequest): DeliveryResponse {
val order = orderRepository.findByOrderIdAndUserId(request.orderId, request.userId)
.orElseThrow { NoSuchElementException("Order not found with orderId: ${request.orderId} and userId: ${request.userId}") }
val deliveryId = UUID.randomUUID().toString()
val orderAfterTryDelivery = processOrderStep(order, Step.TRY_DELIVERY, "deliveryId: $deliveryId, request: $request")
processOrderStep(orderAfterTryDelivery, Step.DONE_DELIVERY, "deliveryId: $deliveryId, request: $request")
return DeliveryResponse(orderId = request.orderId, userId = request.userId, deliveryId = deliveryId, step=Step.DONE_DELIVERY)
}
private fun processOrderStep(order: Order, step: Step, data: String): Order {
val updatedSteps = order.steps?.toMutableList()?.apply { add(OrderStep(step, data)) }
return orderRepository.save(order.copy(steps = updatedSteps))
}
fun getOrder(orderId: String): Optional<Order> {
return orderRepository.findByOrderId(orderId)
}
}
테스트
주문 프로세스의 전체 흐름을 검증하기 위한 테스트 코드는 다음과 같습니다.
@SpringBootTest
class OrderServiceTest{
@Autowired
lateinit var orderService: OrderService
@Test
fun createOrder() {
val order = orderService.createOrder(OrderRequest(
goodsId = "t-001",
goodsName = "테스트 상품",
goodsPrice = 1000.0,
userId = "u-001",
quantity = 1,
))
val payment = orderService.addPayment(PaymentRequest(
orderId = order.orderId,
userId = order.userId,
amount = 1000.0,
paymentType = "CARD",
))
val delivery = orderService.addDelivery(
DeliveryRequest(
orderId = order.orderId,
userId = order.userId,
address = "서울시 강남구",
))
println("order: $order")
val result = orderService.getOrder(order.orderId)
println("result: $result")
}
}
결과
몽고디비의 장점과 단점
장점
유연한 데이터 모델: 몽고디비는 스키마가 없는 NoSQL 데이터베이스로, 다양한 데이터 모델을 손쉽게 저장하고 관리할 수 있습니다.
수평적 확장성: 큰 데이터 세트와 높은 처리량을 처리하기 위해 몽고디비 클러스터를 수평적으로 확장할 수 있습니다.
JSON 형식의 문서 저장: 데이터를 BSON(바이너리 JSON) 형식으로 저장하여 개발자에게 친숙하고 효율적인 데이터 교환을 가능하게 합니다.
강력한 쿼리 언어: 몽고디비는 SQL과 유사한 쿼리 언어를 제공하여 다양한 데이터 검색 및 처리가 가능합니다.
높은 성능: 인덱싱, 샤딩 등의 기능을 통해 빠른 데이터 접근과 처리가 가능합니다.
단점
조인의 제한: 몽고디비는 SQL 데이터베이스에 비해 조인 연산의 지원이 제한적입니다.
트랜잭션 처리: 전통적인 관계형 데이터베이스에 비해 복잡한 트랜잭션 처리가 다소 제한적입니다.
데이터 일관성: 몽고디비는 기본적으로 "최종 일관성" 모델을 사용하며, 이는 일부 사용 사례에서 데이터 일관성 문제를 일으킬 수 있습니다.
결론
몽고디비를 사용하면 유연한 데이터 모델과 빠른 성능을 바탕으로 복잡한 주문 프로세스를 효과적으로 관리할 수 있습니다. 하지만, 데이터 일관성과 트랜잭션 처리에 있어 몇 가지 고려해야 할 점이 있습니다.
'SpringBoot' 카테고리의 다른 글
마이크로서비스 추적을 위한 Spring Cloud Sleuth와 Zipkin 사용예제 (1) | 2023.10.22 |
---|---|
스프링 클라우드 스트림과 카프카를 활용한 마이크로서비스 이벤트 처리 (1) | 2023.10.22 |
스프링의 RedisLockRegistry를 활용한 제한된 상품의 동시성 제어 및 주문 처리 (0) | 2023.10.09 |
스프링 부트에서 외부 API 서비스를 병렬 처리하여 응답하는 방법 (0) | 2023.10.09 |
자바의 Supplier와 Consumer 인터페이스 이해와 활용 (0) | 2023.10.09 |