동시성 문제 (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
'Projects > hub-eleven' 카테고리의 다른 글
| [동시성 처리] 동시성 문제 (Race Condition) 해결 방법 3가지 관점 - (3) Redis Distributed Lock (0) | 2026.03.11 |
|---|---|
| [동시성 처리] 동시성 문제 (Race Condition) 해결 방법 3가지 관점 - (2) Database Lock (0) | 2026.03.09 |
| [동시성 처리] 동시성 문제 (Race Condition) 해결 방법 3가지 관점 - (1) Synchronized (0) | 2026.03.06 |
| [동시성 처리] 재고 감소 통합 테스트 코드 6 - Optimistic Lock(낙관적 락) 멀티 스레드 테스트 코드 (1) | 2026.02.24 |
| [동시성 처리] Optimistic Lock (낙관적 락) 재시도 로직 (0) | 2026.02.23 |