밀키트 도메인의 특성 분석
- 사용자는 유통기한이 5일 이상 남아있는 제품을 받아야함
- 재고의 유통기한을 주기적으로 관리해주어야함 - 유통기한 임박 제품을 사용자에게 제공하면 안됨
- 일자별 재고를 따로 관리해주어야 함
주문 흐름 설계
전체적으로 주문 flow를 동기적으로 구성했음
1. 주문을 하려면 수량을 확보해야함
2. 재고 감소를 반영해야함 (향후 배치 작업으로 분리함)
3. 상품 수량을 감소시켜야함
4. 결제 처리를 진행해야 함 (Toss 테스트 결제 서버)
위의 동기적 방식을 진행하면서 상품 수량에 대해 동시성 이슈가 있기 때문에 비관적 락을 걸어 문제를 해결함
문제 발생
부하 테스트 진행
- EC2 t4small server 대상 테스트입니다
nGrinder을 사용해 vUser 20 기준으로 한 상품 구매 요청 진행
- 결제 지연 시간은 Toss 테스트 서버 기준 평균 1.2초 -> 평균 1.2, 표준편차 0.2 정규 분포로 지연시간 부과
DB connection은 pending 상태이며 TPS는 평균 0.8인 매우 좋지 않은 성능
문제점: 주문과 결제가 한 트랜잭션으로 묶여있음
- DB 커넥션을 획득하고 결제 처리가 끝날 때까지 놓지 않음 (지연 시간 평균 1.2초)
- 비관적 락 때문에 다른 사용자들은 락을 획득하지 못함
문제 해결
트랜잭션 분리 진행
주문 처리를 진행하는 트랜잭션, 결제 처리를 진행하는 트랜잭션을 분리함
//주문 흐름
//No Transaction
public void pay(final AuthPrincipal authPrincipal, final Long orderId, final OrderPayRequest request) {
orderPlaceService.place(authPrincipal, orderId); // @Transactional
orderPayService.pay(orderId, request.getPaymentKey()); // No Transaction
}
// OrderPayService.class
@Counted("order.payment.request")
public void pay(final Long orderId, final String paymentKey) {
log.info("결제 요청 subscribe event: {}", paymentKey);
Order order = orderRepository.findById(orderId).orElseThrow(OrderNotFoundException::new);
paymentClient.validatePayment(paymentKey, order.getUuid(), order.getTotalPrice())
.publishOn(Schedulers.boundedElastic())
.doOnSuccess(ignore -> payResultHandler.save(orderId, paymentKey))
.doOnError(error -> payResultHandler.rollback(orderId, error))
.onErrorMap(IllegalArgumentException.class, InvalidPayRequestException::new)
.onErrorMap(IllegalStateException.class, PayFailedException::new)
.block();
}
위와 같이 트랜잭션을 분리함으로써 DB커넥션을 빨리 놓을 수 있었음
Vuser 20명 기준 평균 TPS 14.6, 평균 응답시간 1.37s
처음에 비해 같은 조건에서 17배의 주문-결제 처리로 개선 (EC2 t4 small 1대 기준)
다양한 상품 구매 테스트 진행 - 추가 진행
20% 상품에 80%의 구매가 몰리도록 설정 (EC2 t4 small 1대 기준)
120 Vser 기준 평균 84 TPS
Load Average/ CPU Core Size가 1을 넘지 않는 기준으로 측정했으며 예상한 시나리오보다 더 충분한 TPS 성능을 보여주었음