4차산업혁명의 일꾼/Spring

동시성테스트

르무엘 2025. 6. 19. 21:59

네, 가능합니다. JUnit만으로도 스프링 부트 애플리케이션의 결제 로직에 대해 동시성(Concurrency) 테스트를 해볼 수 있는데, 대략 다음과 같은 패턴을 씁니다.


---

1. 테스트 구조 개요

1. 멀티스레드로 동시 요청 시뮬레이션
ExecutorService + CountDownLatch를 사용해서 여러 스레드가 “동시에” 결제 API(또는 서비스 메서드)를 호출하도록 함.


2. 실제 빈 주입 & 트랜잭션 관리

@SpringBootTest + @Autowired 로 PaymentService나 TestRestTemplate 등을 주입

테스트에서는 기본 @Transactional 롤백을 끄거나, 실제 커밋이 일어나도록 설정해야 진짜 동시성 이슈(예: 이중 청구 등)를 검증할 수 있음.





---

2. 샘플 코드

@SpringBootTest
@TestInstance(Lifecycle.PER_CLASS)
public class PaymentConcurrencyTest {

    @Autowired
    private PaymentService paymentService;

    // 실제로 DB에 커밋이 일어나도록 롤백을 끔
    @BeforeAll
    void disableRollback(TestInfo info) {
        // 만약 @Transactional 테스트가 기본 롤백이라면
        // @Commit 어노테이션을 클래스나 메서드에 붙여도 됩니다.
    }

    @Test
    @Commit                  // 실제 커밋
    void 동시에_100명_결제_테스트() throws InterruptedException {
        int threads = 100;
        ExecutorService exec = Executors.newFixedThreadPool(threads);
        CountDownLatch readyLatch = new CountDownLatch(threads);
        CountDownLatch startLatch = new CountDownLatch(1);
        CountDownLatch doneLatch  = new CountDownLatch(threads);

        for (int i = 0; i < threads; i++) {
            exec.submit(() -> {
                readyLatch.countDown();      // 준비 완료
                try {
                    startLatch.await();      // 시작 신호 대기
                    // --- 실제 호출: 결제 서비스 메서드 또는 REST API ---
                    paymentService.pay(orderId, amount);
                    // ---------------------------------------------
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    doneLatch.countDown();
                }
            });
        }

        // 모든 스레드가 준비되면
        readyLatch.await();
        // 동시 시작
        startLatch.countDown();
        // 끝날 때까지 대기
        doneLatch.await();
        exec.shutdown();

        // 검증: 중복 결제 방지, 최종 결제 횟수는 1건인가?
        long successCount = paymentRepository.countByOrderId(orderId);
        assertEquals(1, successCount, "동시에 결제 요청이 와도 1건만 처리되어야 합니다.");
    }
}

주요 포인트

1. CountDownLatch

readyLatch: 모든 워커 스레드가 준비됐음을 메인 스레드에 알림

startLatch: 메인 스레드가 동시에 “시작” 신호를 주면 스레드들이 동시에 실행

doneLatch: 모든 작업 완료를 메인 스레드가 대기



2. @Commit (or @Transactional(propagation = NOT_SUPPORTED))

테스트에 기본 붙는 롤백을 끄고 실제 DB 커밋을 해줘야 동시성 문제(예: DuplicateKeyException, 중복처리 등)를 확인할 수 있음.



3. 실제 환경과 최대한 유사하게

테스트용 인메모리 DB(H2) 대신 MySQL/PostgreSQL 컨테이너를 쓰면 더 현실적인 동시성 검증이 가능합니다.(Testcontainers 추천)





---

3. 추가 팁

트랜잭션 격리 레벨 설정을 바꿔보며(READ_COMMITTED, SERIALIZABLE 등) 어떻게 동작이 달라지는지 실험

낙관적 락 vs 비관적 락 전략을 각각 적용해보고, 동시성 시나리오에 맞는 방어 로직을 검증

REST API 계층까지 포함하려면 TestRestTemplate이나 WebTestClient로 스레드 내에서 HTTP 호출



---

이 구조를 활용하시면 JUnit 환경에서도 충분히 결제 로직의 동시성 이슈를 잡아보실 수 있습니다. 필요하시면 더 구체적인 예제(락 처리, 트랜잭션 설정 등)도 제공해드릴게요!


LIST