당사의 주문 처리 시스템은 매일 평균 10만건의 주문을 처리하고 있습니다.
이후 프로모션 기간 동안 주문을 위해 5배인 50만 건까지 증가하면서 다음과 같은 문제가 발생했습니다.
기존 레이어 배치 기반의 시스템은 다음과 동일한 구조적 경계를 설정합니다.
@Service
class OrderService(
private val orderRepository: OrderRepository,
private val paymentClient: PaymentClient,
private val inventoryClient: InventoryClient
) {
@Transactional
fun processOrder(orderRequest: OrderRequest): OrderResult {
// 모든 로직이 하나의 트랜잭션에 묶여있음
val order = orderRepository.save(Order.from(orderRequest))
val payment = paymentClient.process(order.toPaymentRequest())
val inventory = inventoryClient.reserve(order.toInventoryRequest())
return OrderResult(order, payment, inventory)
}
}
시스템을 중심으로 재설계하고, 외부 의존성을 격리하기 위해 해악을 도모하는 날을 배치했습니다.
// Domain
data class Order(
val id: OrderId,
val items: List<OrderItem>,
val status: OrderStatus,
val totalAmount: Money
) {
fun validateStock(inventory: Inventory): Boolean {
return items.all { item ->
inventory.hasStock(item.productId, item.quantity)
}
}
fun calculateDeliveryFee(): Money {
return when {
totalAmount.value >= 50000 -> Money.ZERO
else -> Money(3000)
}
}
}
// Port (Input)
interface OrderUseCase {
fun createOrder(command: CreateOrderCommand): OrderResult
fun cancelOrder(command: CancelOrderCommand): OrderResult
}
// Port (Output)
interface OrderRepository {
fun save(order: Order): Order
fun findById(orderId: OrderId): Order?
}
interface PaymentPort {
fun process(request: PaymentRequest): PaymentResult
fun cancel(paymentId: PaymentId): PaymentResult
}
// Application Service
@UseCase
class OrderService(
private val orderRepository: OrderRepository,
private val paymentPort: PaymentPort,
private val inventoryPort: InventoryPort
) : OrderUseCase {
override fun createOrder(command: CreateOrderCommand): OrderResult {
val order = Order.create(command)
// 재고 확인
require(order.validateStock(inventoryPort.getInventory(order.items))) {
throw OutOfStockException()
}
// 결제 진행
val payment = paymentPort.process(PaymentRequest.from(order))
if (!payment.isSuccess()) {
throw PaymentFailedException()
}
// 주문 저장
return orderRepository.save(order)
}
}
// Adapter (Infrastructure)
@Repository
class OrderJpaAdapter(
private val orderJpaRepository: OrderJpaRepository,
private val orderMapper: OrderMapper
) : OrderRepository {
override fun save(order: Order): Order {
val entity = orderMapper.toEntity(order)
val savedEntity = orderJpaRepository.save(entity)
return orderMapper.toDomain(savedEntity)
}
}
외부 시스템과의 통신 프로세서를 처리하여 성능을 개선했습니다.
@UseCase
class AsyncOrderService(
private val orderRepository: OrderRepository,
private val paymentPort: PaymentPort,
private val inventoryPort: InventoryPort,
private val coroutineScope: CoroutineScope
) : OrderUseCase {
override suspend fun createOrder(command: CreateOrderCommand): OrderResult = coroutineScope.async {
val order = Order.create(command)
// 재고 확인과 결제를 병렬로 처리
val (inventory, payment) = awaitAll(
async { inventoryPort.getInventory(order.items) },
async { paymentPort.process(PaymentRequest.from(order)) }
)
require(order.validateStock(inventory)) {
throw OutOfStockException()
}
if (!payment.isSuccess()) {
throw PaymentFailedException()
}
orderRepository.save(order)
}.await()
}
1. 외부 API 호출 최적화
2. DB 커넥션 풀 최적화
3. 도메인구조 최적화
@Aspect
@Component
class OrderMonitoringAspect(
private val meterRegistry: MeterRegistry
) {
@Around("@annotation(Monitored)")
fun monitorPerformance(joinPoint: ProceedingJoinPoint): Any {
val startTime = System.currentTimeMillis()
val result = joinPoint.proceed()
val duration = System.currentTimeMillis() - startTime
meterRegistry.timer("order.processing.time")
.record(duration, TimeUnit.MILLISECONDS)
return result
}
}
마이그레이션은 단기간에 완료할 수 없으므로 점진적 전환 전략을 사용했습니다.
새로운 시스템이 전체 주문 처리를 담당하기 전에, 레거시 시스템과 새로운 시스템을 병렬로 운영하여 안정성을 확보했습니다.
이를 위해 Blue-Green Deployment 방식을 적용했습니다
- Blue: 기존 레거시 시스템
- Green: 새로운 시스템
적용 방법
새로운 요청 중 10%를 Green 환경으로 라우팅.
오류 발생 시 Blue로 즉시 전환(Fallback).
안정성 확인 후 10%씩 트래픽 증가.
@Configuration
class RoutingConfig {
@Bean
fun routeLocator(builder: RouteLocatorBuilder): RouteLocator {
return builder.routes()
.route("blue-route") {
it.path("/order/**")
.and().header("X-Traffic-Percent", "blue")
.uri("http://blue-legacy-system")
}
.route("green-route") {
it.path("/order/**")
.and().header("X-Traffic-Percent", "green")
.uri("http://green-new-system")
}
.build()
}
}
마이그레이션 중 트래픽 증가로 인해 새로운 시스템에서 병목 현상이 발생하지 않도록 부하 테스트를 미리 수행했습니다.
실제 적용 코드: Gatling 테스트 스크립트
import io.gatling.core.Predef._
import io.gatling.http.Predef._
class OrderServiceLoadTest extends Simulation {
val httpProtocol = http.baseUrl("http://green-new-system")
val scn = scenario("Order Load Test")
.exec(
http("Create Order")
.post("/order")
.body(StringBody("""{
"userId": "12345",
"items": [{"productId": "A1", "quantity": 2}]
}""")).asJson
.check(status.is(200))
)
setUp(
scn.inject(constantUsersPerSec(500).during(60))
).protocols(httpProtocol)
}
새로운 시스템에서 심각한 오류가 발생할 경우를 대비하여 롤백 가능한 시스템을 구축했습니다.
@Component
class RollbackConsumer(
private val legacyOrderRepository: LegacyOrderRepository
) {
@KafkaListener(topics = ["order.rollback"], groupId = "rollback-group")
fun handleRollback(record: ConsumerRecord<String, String>) {
val order = ObjectMapper().readValue(record.value(), LegacyOrder::class.java)
legacyOrderRepository.save(order)
}
}
외부 시스템(API)와의 통신이 병목이었던 기존 구조를 개선하기 위해 Circuit Breaker 패턴을 적용했습니다.
@Service
class PaymentService(
private val paymentClient: PaymentClient
) {
@CircuitBreaker(name = "paymentService", fallbackMethod = "fallbackPayment")
suspend fun processPayment(request: PaymentRequest): PaymentResult {
return paymentClient.process(request)
}
fun fallbackPayment(request: PaymentRequest, ex: Throwable): PaymentResult {
return PaymentResult.failure("Fallback triggered: ${ex.message}")
}
}
Kotlin Coroutines을 활용하여 외부 API 호출과 DB 작업을 병렬로 처리하여 성능을 최적화했습니다.
suspend fun processOrder(command: CreateOrderCommand): OrderResult = coroutineScope {
val order = Order.create(command)
val (inventory, payment) = awaitAll(
async { inventoryPort.getInventory(order.items) },
async { paymentPort.process(PaymentRequest.from(order)) }
)
require(order.validateStock(inventory)) { throw OutOfStockException() }
if (!payment.isSuccess()) { throw PaymentFailedException() }
orderRepository.save(order)
}
Prometheus와 Grafana를 활용하여 시스템 성능을 실시간으로 모니터링했습니다.
실제 코드: Micrometer로 성능 데이터 수집
@Component
class OrderMetrics(
private val meterRegistry: MeterRegistry
) {
fun recordOrderProcessingTime(duration: Long) {
meterRegistry.timer("order.processing.time")
.record(duration, TimeUnit.MILLISECONDS)
}
fun incrementErrorCount() {
meterRegistry.counter("order.errors.count").increment()
}
}
코드 예시: Fetch Join
@Repository
interface OrderRepository : JpaRepository<OrderEntity, Long> {
@Query("SELECT o FROM OrderEntity o JOIN FETCH o.items WHERE o.id = :orderId")
fun findOrderWithItems(orderId: Long): OrderEntity
}
주문 생성 및 처리를 이벤트 기반 아키텍처로 전환.
이벤트 예시
@KafkaListener(topics = ["order.created"])
fun handleOrderCreated(event: String) {
val order = ObjectMapper().readValue(event, Order::class.java)
processOrder(order)
}
Redis를 활용한 캐싱으로 API 호출과 DB 부하를 줄이는 작업 예정.
모놀리식 시스템을 마이크로서비스로 전환하여 독립적 배포 및 확장성을 확보.
해악을 끼치는 일과 코틀린 코루틴의 교체로 시스템의 성능과 비교하면 크게 개선되었습니다.
특히 실리콘의 절연 및 안정성 처리를 통해 복잡한 비즈니스 요구사항을 처리할 수 있을 가능성이 더 높습니다.