쿠폰 프로모션 프로젝트를 진행하며 쿠폰 발급 시 동시성 문제가 발생하는지 확인하는 테스트 코드를 작성해야 했다.
그런데 그동안 순차적으로 진행되는 테스트 코드만 작성해보아서 동시성 테스트 코드를 어떻게 작성하는지 알지 못했다.
그래서 동시성 테스트 코드는 어떻게 작성하는지 공부하고 그 내용을 정리해보려고 한다.
프로젝트 레포지토리: https://github.com/woowa-coupons/woowa-coupons
참고차 현재 프로젝트에서 쿠폰을 발급하는 코드의 일부를 발췌해 왔다.
쿠폰 발급 시, 프로모션의 조건을 확인하고 회원이 발급받을 수 있는 조건을 확인한 후 쿠폰을 발급한다.
@Transactional
public void issueCoupon(CouponIssueRequest request, Member member) {
// 프로모션의 조건을 찾고
List<PromotionOption> promotionOptions = promotionOptionRepository.findByPromotionId(request.promotionId());
// 회원이 발급받을 수 있는 쿠폰 조건 확인
CouponGroup allMatchedCouponGroup = promotionOptions.stream()
.filter(promotionOption -> isMemberSatisfied(member, promotionOption.getConditions()))
.map(this::getCouponGroups)
.findFirst()
.flatMap(couponGroups -> couponGroups.stream()
.filter(this::hasRemainCoupon)
.filter(couponGroup -> !isExpiredCouponGroup(couponGroup))
.filter(couponGroup -> !isAlreadyIssued(member, couponGroup))
.findFirst())
.orElseThrow(() -> new ApiException(CouponGroupException.NOT_FOUND));
// 쿠폰 발급
issueCouponInCouponGroup(allMatchedCouponGroup, member);
}
쿠폰이 발급되면 잔여 수량에서 -1을 한다.
@Builder
private Coupon() {
// 생략
}
public void issue() {
if (this.remainQuantity <= 0) {
throw new ApiException(CouponException.EXHAUSTED);
}
this.remainQuantity--;
}
그러면 이제 여러 회원이 동시에 쿠폰을 발급하면 잔여 수량만큼 발급되는지 확인이 필요하다.
동시성 테스트를 위한 방법을 찾아보았는데 CountDownLatch
와 ExecutorService
를 활용한 예제 코드가 많았다.
우선 예제 코드를 먼저 보고 CountDownLatch
와 ExecutorService
에 대해 알아보자.
테스트 코드 흐름을 정리해보면 다음과 같다.
ExecutorService
를 통해 스레드 풀 생성- 생성된 스레드 풀에서 비동기로 쿠폰 발급 요청
- 요청이 성공했을 경우
successCount
+1,실패했을 경우 failCount
+1CountDownLatch
를 통해 비동기 작업이 끝났는지 확인- 테스트 결과 학인
@Test
void issueCoupon() throws InterruptedException {
// ---- 테스트를 위한 데이터 준비 ----
// given
Long promotionId = savePromotion(); // 쿠폰 발급을 위해 프로모션 생성
int couponAmount = CouponFixture.추석_쿠폰_신규.getInitialQuantity(); // 쿠폰 개수
int memberCount = couponAmount + 100; // 동시 요청하는 회원수
ExecutorService executorService = Executors.newFixedThreadPool(30); // 스레드 풀 생성
CountDownLatch latch = new CountDownLatch(memberCount); // CountDownLatch 생성
AtomicInteger successCount = new AtomicInteger();
AtomicInteger failCount = new AtomicInteger();
// 쿠폰 발급에 필요한 회원 가입
List<Member> members = new ArrayList<>();
for (int i = 0; i < memberCount; i++) {
Member member = 랜덤_회원_가입();
members.add(member);
}
// ---- 쿠폰 발급 ----
// when
for (int i = 0; i < memberCount; i++) {
Member member = members.get(i);
executorService.execute(() -> {
try {
memberCouponService.issueCoupon(new CouponIssueRequest(promotionId), member);
successCount.incrementAndGet(); // 쿠폰 발급에 성공하면 successCount 증가
} catch (Exception e) {
failCount.incrementAndGet(); // 쿠폰 발급에 실패하면 failCount 증가
} finally {
latch.countDown();
}
});
}
latch.await();
// ---- 테스트 결과 확인 ----
// then
List<MemberCoupon> memberCoupons = supportRepository.findAll(MemberCoupon.class);
assertThat(memberCoupons.size()).isEqualTo(couponAmount); // 발급 받은 쿠폰 수가 발급된 쿠폰 개수와 일치하는지 확인
assertThat(successCount.get()).isEqualTo(couponAmount);
assertThat(failCount.get()).isEqualTo(100);
}
ExecutorService
는 java.util.concurrent
에서 제공하는 클래스다.
공식 문서에 따르면 ExecutorService
는 Executor
를 상속받은 인터페이스로, 동시에 여러 작업들을 싱행시키는 메서드를 제공하고 하나 이상의 비동기 작업 진행 상태를 추적하고 관리할 수 있는 메서드를 제공해주는 클래스이다.
An Executor that provides methods to manage termination and methods that can produce a Future for tracking progress of one or more asynchronous tasks.
비동기 작업을 지원하는 메서드 일부를 살펴보면 execute()
와 submit()
이 있다.
execute()
:Runnable
인터페이스void
submit()
: Runnable
과 Callable
인터페이스Future
객체테스트 코드 예제를 찾아보았을 때 두가지 메서드를 많이 사용하고 있었는데,
작업 결과가 필요하면 submit()
을 쓰고 비동기 작업만 실행할 것이라면 execute()
를 사용하면 될 것 같다는 생각이다.
💡 참고:
execute()
는Executor
인터페이스에 정의 되어 있고,submit()
은ExecutorService
인터페이스에 정의되어 있다.ExecutorService
는Executor
를 상속받았기 때문에 두 메서드 모두 호출할 수 있다.
CountDownLatch
도 java.util.concurrent
에서 제공하는 클래스다.
공식 문서에 따르면 CountDownLatch
는 1개 혹은 그 이상의 스레드가 다른 스레드의 작업이 완료될 때까지 기다릴 수 있게 도와주는 클래스이다.
A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.
CountDownLatch
메서드테스트 코드에 활용한 메서드를 살펴보자.
Latch
숫자 전달CountDownLatch latch = new CountDownLatch(memberCount);
countDown()
Latch
숫자가 1씩 감소latch.countDown();
await()
Latch
의 숫자가 0이 될 때까지 대기latch.await();
CountDownLatch
가 필요할까?쿠폰 발급을 비동기 작업으로 여러 스레드에서 처리하면 모든 스레드의 작업이 언제 끝났는지 알 수가 없다.
따라서 CountDownLatch
를 활용해 모든 스레드의 작업이 끝났는지 확인이 필요하다.
CountDownLatch
를 생성할 때 Latch
수를 작업 횟수만큼 설정하고, 작업이 한번 실행할 때마다 latch.countDown()
를 호출한다.
latch.await()
를 통해 작업이 끝날 때까지 대기 후, Assertions
으로 테스트 결과를 확인한다.
잘읽었습니다 좋은 글 감사합니다👍