동시성 제어에 대한 관심이 많아서, 선착순 쿠폰 발급 서비스를 개발해 보기로 결정했습니다. 다양한 상황에서 기술을 하나씩 적용해보면서, 이론으로만 공부했던 동시성 제어 문제에 대해 실전에 적용하는 경험을 가지려고 합니다.
개발 뿐만 아니라, 동시성을 확인할 수 있는 테스트에 관해서도 고민해보고 포스팅을 할 예정입니다.
백엔드 서버로는 자바 스프링에 JPA를 사용할 예정입니다.
DB 는 먼저 RDB - MySQL 을 사용해서 구현해보고, 필요에 따라 다른 NoSQL 데이터베이스(레디스, 몽고DB) 를 사용해볼 생각입니다.
먼저 단일 WAS 서버와 단일 데이터베이스가 있는 가장 간단한 사례 부터 확장해 나가겠습니다.
향후, 다음과 같은 아주 단순한 구조의 쿠폰 엔티티를 사용할 예정입니다.
quantity 는 쿠폰 초기 발매 수량을 뜻합니다.
@Entity
@Getter
public class Coupon {
@Id
private Long id;
private int quantity;
public void getCoupon() {
this.quantity--;
}
}
동시성을 고려하지 않고, 가장 간단히 쿠폰 발급 서비스로직을 짜보자면 다음과 같을겁니다.
@Transactional
public void getCoupon(Long id) {
Coupon coupon = couponRepository.findById(id).orElseThrow(RuntimeException::new);
if(coupon.getQuantity() <= 0) {
throw new RuntimeException();
}
coupon.getCoupon();
}
ExecutorService 를 이용해 동시성 검증을 해보기로 했습니다.
초기 쿠폰 개수 50000
쓰레드 10개
고객 1000명 요청
@BeforeEach
void setUp() {
coupon = new Coupon(5L, 50000);
couponRepository.save(coupon);
}
@Test
void getCoupon() {
int threadCount = 10;
int customerCount = 1000;
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(customerCount);
for(int i = 0; i < customerCount; i++) {
executorService.execute(() -> {
couponController.getCoupon(coupon.getId());
latch.countDown();
});
}
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Coupon c = couponRepository.findById(coupon.getId()).get();
Assertions.assertThat(c.getQuantity()).isEqualTo(coupon.getQuantity() - customerCount);
}
예상 : 49000
결과 : 49899
고객이 1000명이라 가정했으므로, 모든 요청이 종료된 후, 쿠폰의 개수는 49000 이어야 합니다.
하지만, 테스트 해본 결과 49899 가 나왔습니다.
동시성을 고려하지 않았기에 문제가 발생했습니다. 다음 장에서 부터는 해결책을 하나하나 적용해 나갈 예정입니다.