Projects/HubEleven

[동시성 처리] 재고 감소 통합 테스트 코드 7 - 멀티 스레드

annovation 2026. 2. 3. 23:27

멀티 스레드 테스트 코드

💡StockCurrencyTest.java

package com.hubEleven.product.stock.application.service;

import static org.assertj.core.api.Assertions.assertThat;

import com.hubEleven.product.domain.model.Product;
import com.hubEleven.product.infrastructure.repository.JpaProductRepository;
import com.hubEleven.product.stock.application.fixtures.ProductFixture;
import com.hubEleven.product.stock.application.fixtures.StockFixture;
import com.hubEleven.stock.application.service.StockServiceImpl;
import com.hubEleven.stock.domain.model.Stock;
import com.hubEleven.stock.infrastructure.repository.JpaStockRepository;
import com.hubEleven.stock.presentation.dto.request.StockRequests;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class StockCurrencyTest {

    @Autowired
    private StockServiceImpl stockServiceImpl;

    @Autowired
    private JpaStockRepository jpaStockRepository;

    @Autowired
    private JpaProductRepository jpaProductRepository;

    @BeforeEach
    void setUp() {
        Product p = ProductFixture.createDefault();
        Product product = jpaProductRepository.saveAndFlush(p);

        Stock aDefault = StockFixture.createDefault(product.getProductId());

        jpaStockRepository.saveAndFlush(aDefault);
    }

    @AfterEach
    void tearDown() {
        jpaStockRepository.deleteAll();
        jpaProductRepository.deleteAll();
    }

    @Test
    @DisplayName("재고 감소 - 100개 동시 요청 시 정확히 차감")
    void decreaseStock_when100ConcurrentRequests_thenSuccess() throws InterruptedException {

        // given
        int threadCount = 50;
        int decreaseAmountPerCall = 1; // 50명이 1개씩 감소 => 성공 50이면 100-50 = 50

        Product product = jpaProductRepository.findAll().get(0);

        ExecutorService pool = Executors.newFixedThreadPool(threadCount);

        CountDownLatch readyLatch = new CountDownLatch(threadCount);
        CountDownLatch startLatch = new CountDownLatch(1);
        CountDownLatch doneLatch = new CountDownLatch(threadCount);

        AtomicInteger successCount = new AtomicInteger();
        AtomicInteger failureCount = new AtomicInteger();

        for (int i = 0; i < threadCount; i++) {
            pool.submit(() -> {
                readyLatch.countDown();
                try {
                    startLatch.await();

                    StockRequests.Decrease req =
                            StockFixture.decreaseRequest(decreaseAmountPerCall,
                                    product.getProductId());

                    stockServiceImpl.decreaseStock(req);
                    successCount.incrementAndGet();
                } catch (Exception e) {
                    // 재고부족/락 충돌/낙관적락 등 어떤 예외든 실패로 집계
                    failureCount.incrementAndGet();
                } finally {
                    doneLatch.countDown();
                }
            });
        }

        // 모든 스레드 준비 -> 동시에 출발 -> 종료 대기
        readyLatch.await();
        startLatch.countDown();
        doneLatch.await();
        pool.shutdown();

        // then
        Stock updated =
                jpaStockRepository.findAll().get(0);
        int initial = 100;

        assertThat(updated.getQuantity()).isEqualTo(50);
    }
}