종우의 삶 (전체 공개)

MSA Project - 대용량 요청 처리 테스트 (JMeter) 본문

개발/Spring

MSA Project - 대용량 요청 처리 테스트 (JMeter)

jonggae 2024. 7. 25. 14:16

기존의 테스트가 로그도 부실하고 쓸모없는 체크과정이 많아서 좀 변경해보았다.

 

Controller에서는 간단한 로깅만 실시하고

Service 쪽에서 더 상세한 로그를 나누어 보았다.

 

@Transactional
public OrderResponse createTimeOrder(Long customerId, Long productId, Long quantity) {
    log.info("주문 시작 - CustomerId: {}, ProductId: {}, Quantity: {}", customerId, productId, quantity);

    if (stockExhausted.get()) {
        log.warn("재고 소진 - CustomerId: {}, ProductId: {}", customerId, productId);
        return new OrderResponse(null, OrderResult.STOCK_INSUFFICIENT);
    }

    ProductDto product = productClient.getProductOrderInfo(productId);
    RLock lock = redissonClient.getLock(LOCK_PREFIX + productId);
    boolean stockReserved = false;

    try {
        if (lock.tryLock(LOCK_WAIT_TIME, LOCK_LEASE_TIME, TimeUnit.MILLISECONDS)) {
            try {
                stockReserved = productClient.reserveStock(productId, new StockReservationRequestDto(quantity));
                if (!stockReserved) {
                    log.warn("재고 부족 - CustomerId: {}, ProductId: {}, Quantity: {}", customerId, productId, quantity);
                    return new OrderResponse(null, OrderResult.STOCK_INSUFFICIENT);
                }
            } finally {
                lock.unlock();
            }
        } else {
            log.warn("동시 주문 충돌 - CustomerId: {}, ProductId: {}", customerId, productId);
            return new OrderResponse(null, OrderResult.CONCURRENT_ORDER_CONFLICT);
        }

        Order order = createOrder(customerId, product, quantity);
        Order savedOrder = orderRepository.save(order);
        log.info("주문 생성 - OrderId: {}, CustomerId: {}", savedOrder.getId(), customerId);

        PaymentResult paymentResult = processPayment(savedOrder.getId());
        log.info("결제 처리 결과 - OrderId: {}, Result: {}", savedOrder.getId(), paymentResult);

        OrderStatus finalStatus = updateOrderStatus(savedOrder, paymentResult, productId, quantity);
        log.info("주문 상태 업데이트 - OrderId: {}, FinalStatus: {}", savedOrder.getId(), finalStatus);

        Order updatedOrder = orderRepository.save(savedOrder);
        OrderDto orderDto = OrderDto.from(updatedOrder);
        OrderResult result = (finalStatus == OrderStatus.PAYMENT_COMPLETE) ? OrderResult.SUCCESS : OrderResult.PAYMENT_FAILED;

        return new OrderResponse(orderDto, result);

    } catch (InterruptedException e) {
        log.error("락 획득 중 인터럽트 발생 - CustomerId: {}, ProductId: {}", customerId, productId);
        Thread.currentThread().interrupt();
        return new OrderResponse(null, OrderResult.CONCURRENT_ORDER_CONFLICT);
    } catch (FeignException e) {
        log.error("외부 서비스 통신 오류 - CustomerId: {}, ProductId: {}", customerId, productId, e);
        if (stockReserved) {
            productClient.cancelStockReservation(productId, quantity);
        }
        return new OrderResponse(null, OrderResult.EXTERNAL_SERVICE_ERROR);
    }
}

서비스 쪽에서 어떤 결과가 나오는지 잘 나누어주고, OrderResult enum 값으로 반환할 것이다.

 

TimeOrderTestController.java

private String getResultMessage(OrderResult result) {
    return switch (result) {
        case SUCCESS -> "테스트 예약 주문 생성 및 결제 완료";
        case PAYMENT_FAILED -> "테스트 예약 주문 생성 실패: 결제 실패";
        case STOCK_INSUFFICIENT -> "테스트 예약 주문 생성 실패: 재고 부족";
        case CONCURRENT_ORDER_CONFLICT -> "테스트 예약 주문 생성 실패: 동시 주문 충돌";
        case EXTERNAL_SERVICE_ERROR -> "테스트 예약 주문 생성 실패: 외부 서비스 오류";
        default -> "테스트 예약 주문 상태 불명확";
    };
}

 

이러한 메시징 메서드를 추가하여 정확한 로그를 출력한다.

 


또한 JMeter로 테스트를 진행해 보았는데, 더 상세한 테스트가 가능 할듯하다.

 

우선 기존의 테스트를 다시 진행해본다.

1만개의 요청을 10초에 걸쳐 보내보았다. -> 점진적 부하에 중요한 관점으로 이후 30초, 60초 등 변경이 될 수 있음.

TPS (Transaction per Second)

 

-> 시스템이 초당 처리할 수 있는 요청의 수 높은 TPS는 시스템의 높은 처리 능력을 의미 (동시에 많은 사용자를 처리)

-> 부하가 증가함에 따라 TPS가 선형적으로 증가하면 좋은 확장성을 뜻함

-> 특정 지점에서 정체되거나 감소하면 병목 지점을 식별 가능

 

Active Threads Over Time

-> 시간에 따른 동시에 활성화된 스레드의 수. 각 스레드는 일반적으로 하나의 동시 사용자 또는 요청을 뜻함

-> 수치가 높다면 많은 동시 요청을 처리할 수 있음을 의미한다.

 

 

그래프로 Transcation per Second와 Active Threads Over Time을 출력해보았다.

 

1차테스트)

1만 개의 요청, 부하 시간 10초

Transaction per Second
Active Threads Over Time

 

 

2차 테스트)

1만 개의 요청, 부하 시간 10초

Transaction per Second
Active Threads Over Time

우선 1, 2차에 걸친 테스트 결과 형태는 비슷하게 출력되었다.

 

재고가 전부 소진되는 시점에서 요청 실패는 급격하게 증가하고, 부하량은 줄어드는 것이다.

 

이는 부하의 증가 시간을 늘리면 더 명확하게 보여지는데, 

 

3차 테스트)

1만 개의 요청, 부하 시간 60초


테스트 결과)

 

분석 

  • 주문을 비동기적으로 처리하고 있기 때문에, 곧바로 재고가 감소하는 것이 아니라 시간이 소요될 수 있다.
  • 분산 락을 이용해 동시성을 제어하므로, 순차적으로 주문이 처리가 된다.
  • 트랜잭션이 끝나야 재고가 감소하므로 지속적으로 주문 성공 요청이 남을 것이다.

개선 방안

  • 불필요한 지연을 제거하고 프로세스를 간소화
  • 비동기적인 주문 요청을 개선 -> Kafka

이후 같은 환경에서 테스트를 진행하면 처리량의 증가, 응답시간 감소 등 변화가 나타날 것이다.

Comments