Projects/hub-eleven

[동시성 처리] 동시성 문제 (Race Conditon) 해결하기

annovation 2026. 5. 11. 23:20

동시성 문제 (Race Conditon) 발생

💡StockCurrencyTest.java

@ActiveProfiles("test")
@SpringBootTest
public class StockCurrencyTest {

	@Autowired
	private StockServiceImpl stockServiceImpl;

	@Autowired
	private JpaStockRepository jpaStockRepository;

	@Autowired
	private JpaProductRepository jpaProductRepository;

	private Product product;

	@BeforeEach
	void setUp() {
		product = ProductFixture.createDefault();
		jpaProductRepository.saveAndFlush(product);

		Stock stock = StockFixture.createFromProductWithQuantity(product, 100);
		jpaStockRepository.saveAndFlush(stock);
	}

	@AfterEach
	void tearDown() {
		jpaStockRepository.deleteAll();
		jpaProductRepository.deleteAll();
	}

	@Test
	@DisplayName("재고 감소 - 100개 동시 요청 시 정확히 차감")
	void decreaseStock_when100ConcurrentRequests_thenSuccess() throws Exception {

		// given
		int threadCount = 100;

		ExecutorService executorService = Executors.newFixedThreadPool(threadCount);

		CountDownLatch countDownLatch = new CountDownLatch(threadCount);

		// when
		for (int i = 0; i < threadCount; i++) {
			executorService.submit(() -> {

				try {
					stockServiceImpl.decreaseStock(
						StockFixture.decreaseRequest(product, 1)
					);
				} finally {
					countDownLatch.countDown();
				}
			});
		}

		countDownLatch.await();

		// then
		Stock updatedStock = jpaStockRepository
			.findByProductId(product.getProductId())
			.orElseThrow();

		assertEquals(0, updatedStock.getQuantity());
	}
}
  • 주문 생성 API에서 동시에 재고 차감 요청 발생
  • 같은 재고 값을 여러 트랜잭션이 읽으면서 Lost Update 발생

원인 분석

💡Stock.java

public void decreaseQuantity(int decreaseQuantity) {
    validateDecreaseQuantity(decreaseQuantity);

    if (this.quantity < decreaseQuantity) {
        throw new GlobalException(INSUFFICIENT_STOCK);
    }

    this.quantity -= decreaseQuantity;
}
  • 조회 후 수정 방식의 재고 차감 로직
  • 기본 트랜잭션 격리 수준에서는 같은 row를 순차적으로 갱신하지 않음
  • 서비스가 여러 인스턴스로 확장될 경우 JVM 내부 락은 무의미

해결하기

💡Pessimistic Lock

  • DB row에 직접 쓰기 락을 걸어 동일 재고 차감 요청을 순차 처리
  • DB 트랜잭션 안에서 정합성을 보장하므로 구현과 데이터 일관성 관리가 단순함
  • 단점은 동시 요청이 몰릴수록 락 대기 시간이 증가하고 DB 부하가 커질 수 있음

💡Redisson Distributed Lock

  • Redis에 productId 기준 락을 걸어 애플리케이션 진입 전부터 요청을 직렬화
  • 여러 Product 서비스 인스턴스가 떠 있어도 동일 Redis를 기준으로 락을 공유할 수 있음
  • 단점은 Redis 장애, 락 타임아웃, 락 해제 실패 같은 운영 고려사항이 추가됨

참고 자료

1) 블로그 : 티켓팅 서비스 동시성 제어) 낙관적 락의 한계와 Redis 분산 락으로의 전환

https://medium.com/@Jinpyo-An/티켓팅-서비스-동시성-제어-낙관적-락의-한계와-redis-분산-락으로의-전환-175093d17898

 

티켓팅 서비스 동시성 제어: 낙관적 락의 한계와 Redis 분산 락으로의 전환

들어가며: 10개의 좌석, 1,000명의 동시 접속

medium.com