문제 상황
동시성 이슈를 고려해 상품의 재고량을 감소시키는 토이프로젝트를 할 때 발생한 문제다.
동시성을 고려한 테스트를 위해 아래와 같이 코드를 작성했다.
@Transactional
@DisplayName("멀티 스레드 환경에서 상품 재고를 감소한다.")
@Test
void findAllProductsByNosWhenMultiThreadEnv() throws InterruptedException {
// given
Product product = createProduct(1000L, "1번 상품", 2000, 100);
productRepository.save(product);
OrderRequestDto orderRequestDto = new OrderRequestDto();
orderRequestDto.addProduct(1000L, 1);
int threadCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i=0; i<threadCount; i++) {
executorService.submit(() -> {
try {
productService.findProductsByNos(orderRequestDto);
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await();
// when
Product result = productRepository.findByNo(1000L)
.orElseThrow(() -> new StockException(ResponseCode.E1000));
// then
assertThat(result.getStock()).isNotEqualTo(100);
}
해당 로직대로 테스트를 수행하게 되면 동시성 이슈에 대한 부분을 처리하지 않기 때문에 100개 중 0보다큰 n개가 감소되어야 한다. 하지만 실행 결과는 재고수 100이 그대로 남아있다.
왜 이러한 현상이 벌어졌을까?
문제 원인
스레드 하나로 접근할 때는 전혀 문제없이 동작했다. 하지만 동시성 이슈를 위한 테스트를 하며 여러개의 스레드로 동시에 접근하니 문제가 발생했다. 결론부터 말하자면 같은 테스트내에서 상품을 저장하고 조회하는 부분에 있었다.
해당 상황을 그림으로 알아보자.
그림에서 볼 수 있듯 테스트 트랜잭션이 시작되고 상품을 먼저 저장한다. 상품을 저장하는 Thread-1의 트랜잭션이 종료되기 전에 Thread-2에서 상품을 조회하면 DB에 데이터가 없어 조회를 못한다.
원인은 트랜잭션의 격리수준에 있었다.
Thread-1에서 시작한 트랜잭션은 Spring의 @Transactional을 활용한다. @Transactional은 모든 로직이 끝나고 마지막에 commit 또는 rollback을 수행하게 되는데, 이 때 실제로 DB에 데이터가 반영된다.
@Transactional의 격리수준을 별도로 설정하지 않았고, MySQL을 사용하기 때문에 기본 격리수준은 REPEATABLE_READ로 설정이 되어 있다.
(만약 MySQL에서 READ_COMMITED로 설정되어 있다고 해도, JPA의 영속성 컨텍스트는 애플리케이션 수준에서 REPEATABLE_READ를 제공한다.)
그렇기 때문에 Thread-2에선 commit되지 않은 데이터를 읽을 수 없어 재고가 전혀 감소되지 않은 문제가 생겼던 것이다.
해결 방안
핵심은 조회를 하는 트랜잭션이 시작되기 전에 데이터가 DB에서 commit되기만 하면 된다.
구체적으로 data.sql을 사용해 데이터를 테스트 전에 저장하거나, @Postconstruct로 빈 초기화 직후에 데이터를 직접 저장해도 된다.
여기선 작은 토이 프로젝트이니 테스트에서 데이터를 저장했지만, 실제론 @Sql, data.sql 등을 활용해 테스트 데이터를 미리 초기화 해놓기 때문에 위와 같은 문제가 발생할 가능성은 적다.
'Spring' 카테고리의 다른 글
Spring - 요청 파라미터 LocalDate타입 필드에 바인딩 및 검증 (1) | 2024.04.12 |
---|---|
Spring - Springboot + Vue3 + MariaDB를 카페24에 배포할 때 Tip(주의사항) (1) | 2023.07.26 |
Spring - ArgumentResolver (0) | 2023.07.22 |
Spring - DispatcherServlet에 대해 알아보자 (0) | 2023.07.15 |