본문 바로가기

Study

TIL - ExecutorService와 CountDownLatch로 동시성 테스트하기

728x90
반응형

쿠폰 재고가 1개일 때 100명이 동시에 발급 요청을 보내도 쿠폰이 1개만 발급되는지 검증하고 싶었다.

핵심 개념

ExecutorService

여러 작업을 스레드로 실행하고 관리하는 도구다.

 
ExecutorService executor = Executors.newFixedThreadPool(100);
 

100개의 작업을 동시에 실행할 수 있는 스레드 풀을 만든다.

 
executor.submit(() -> couponService.issueCoupon());
 

submit()을 호출하면 전달한 작업이 스레드 풀에서 실행된다.

CountDownLatch

내부 카운터가 0이 될 때까지 스레드를 기다리게 하는 동기화 도구다.

 
CountDownLatch latch = new CountDownLatch(100);
 

카운터가 100에서 시작한다.

 
latch.countDown();
 

호출할 때마다 카운터가 1씩 감소한다.

 
latch.await();
 

카운터가 0이 될 때까지 현재 스레드가 기다린다.

테스트에서 사용하는 세 가지 Latch

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

각 역할은 다음과 같다.

Latch역할
readyLatch 100개 작업이 모두 출발 준비를 마쳤는지 확인
startLatch 대기 중인 작업을 한꺼번에 출발시키는 신호
doneLatch 100개 작업이 모두 종료됐는지 확인

예제 코드

 
@Test
void 쿠폰은_동시에_요청해도_한_개만_발급된다() throws Exception {
    int requestCount = 100;

    ExecutorService executor =
            Executors.newFixedThreadPool(requestCount);

    CountDownLatch readyLatch =
            new CountDownLatch(requestCount);

    CountDownLatch startLatch =
            new CountDownLatch(1);

    CountDownLatch doneLatch =
            new CountDownLatch(requestCount);

    List<Future<Boolean>> results = new ArrayList<>();

    for (int i = 0; i < requestCount; i++) {
        Future<Boolean> result = executor.submit(() -> {
            try {
                readyLatch.countDown();
                startLatch.await();

                return couponService.issueCoupon();
            } finally {
                doneLatch.countDown();
            }
        });

        results.add(result);
    }

    readyLatch.await();
    startLatch.countDown();
    doneLatch.await();

    long successCount = results.stream()
            .filter(result -> {
                try {
                    return result.get();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            })
            .count();

    assertThat(successCount).isEqualTo(1);
    assertThat(couponService.getIssuedCount()).isEqualTo(1);

    executor.shutdown();
}
 

실행 흐름

100개 쿠폰 발급 작업 제출
        ↓
각 작업이 readyLatch를 감소
        ↓
모든 작업이 startLatch 앞에서 대기
        ↓
readyLatch가 0이 되면 모두 준비 완료
        ↓
startLatch.countDown()으로 동시 출발
        ↓
100개 작업이 쿠폰 발급 요청
        ↓
doneLatch가 0이 될 때까지 대기
        ↓
성공 건수와 최종 발급 수량 검증
 

배운 점

ExecutorService만 사용하면 여러 작업을 실행할 수는 있지만, 모든 작업이 같은 시점에 시작된다고 보장하기 어렵다.

CountDownLatch를 함께 사용하면 모든 작업을 출발선에 대기시킨 뒤 한꺼번에 실행할 수 있어, 동시성 충돌 상황을 더 확실하게 재현할 수 있다.

실제 서비스에서는 synchronized뿐 아니라 DB 락, 유니크 제약 조건, 조건부 업데이트 등을 사용해 정합성을 보장해야 한다.

반응형