728x90
간단한 재고 시스템으로 알아보는 동시성 이슈
Stock
@Entity
public class Stock {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long productId;
private Long quantity;
@Version
private Long version;
public Stock(){
}
public Stock(Long productId, Long quantity) {
this.productId = productId;
this.quantity = quantity;
}
public Long getQuantity() {
return quantity;
}
public void decrease(Long quantity) {
if (this.quantity - quantity < 0) {
throw new RuntimeException("foo");
}
this.quantity = this.quantity - quantity;
}
}
StockRepository
public interface StockRepository extends JpaRepository<Stock, Long> {
@Lock(value = LockModeType.PESSIMISTIC_WRITE)
@Query("select s from Stock s where s.id = :id")
Stock findByIdWithPessimisticLock(Long id);
@Lock(value = LockModeType.OPTIMISTIC)
@Query("select s from Stock s where s.id = :id")
Stock findByIdWithOptimisticLock(Long id);
}
StockService
@Service
public class StockService {
private StockRepository stockRepository;
public StockService(StockRepository stockRepository) {
this.stockRepository = stockRepository;
}
@Transactional
public void decrease(Long id, Long quantity) {
// get stock
// 재고 감소
// 저장
Stock stock = stockRepository.findById(id).orElseThrow();
stock.decrease(quantity);
stockRepository.saveAndFlush(stock);
}
}
Test코드
@SpringBootTest
class StockServiceTest {
@Autowired
private StockService stockService;
@Autowired
private StockRepository stockRepository;
@BeforeEach
public void before() {
Stock stock = new Stock(1L, 100L);
stockRepository.saveAndFlush(stock);
}
@AfterEach
public void after() {
stockRepository.deleteAll();
}
@Test
public void stock_decrease() {
stockService.decrease(1L, 1L);
// 100 - 1 = 99
Stock stock = stockRepository.findById(1L).orElseThrow();
assertEquals(99, stock.getQuantity());
}
@Test
public void 동시에_100개의_요청() throws InterruptedException {
int threadCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
try {
stockService.decrease(1L, 1L);
} finally {
{
latch.countDown();
}
}
});
}
latch.await();
Stock stock = stockRepository.findById(1L).orElseThrow();
// 100 - (1 * 100) = 0
assertEquals(0L, stock.getQuantity());
}
}
- 하나의 요청이 있을 경우에는 문제 없이 진행되지만, 여러 요청이 있을 경우 스레드가 꼬이면서 제대로 된 결과가 나오지 않는다.
- 이를 Race Condition이라고 하는데, 좀더 자세한 설명은
race condition이란 두 개 이상의 프로세스가 공통 자원을 병행적으로(concurrently) 읽거나 쓰는 동작을 할 때, 공용 데이터에 대한 접근이 어떤 순서에 따라 이루어졌는지에 따라 그 실행 결과가 같지 않고 달라지는 상황을 말한다.
- 예상한 흐름
쓰레드1 | 재고 | 쓰레드2 |
select * from stock where id = 1 | id: 1, quantity: 5 | |
update set queantity = 4 from stock where id = 1 |
id: 1, quantity: 4 | |
id: 1, quantity: 4 | select * from stock where id = 1 | |
id: 1, quantity: 3 | update set queantity = 3 from stock where id = 1 |
- 실제 흐름
쓰레드1 | 재고 | 쓰레드2 |
select * from stock where id = 1 | id: 1, quantity: 5 | |
id: 1, quantity: 5 | select * from stock where id = 1 | |
update set queantity = 4 from stock where id = 1 |
id: 1, quantity: 4 | |
id: 1, quantity: 4 | update set queantity = 4 from stock where id = 1 |
- 이러한 문제를 해결하기 위해서는 데이터에 하나의 쓰레드만 접근이 가능하도록 하면 된다!
- 강의 수강 후 정리내용입니다.
728x90
'개발 > Java&Kotlin' 카테고리의 다른 글
[Spring] 동시성 이슈 해결 방법 (3) (0) | 2022.09.22 |
---|---|
[Spring] 동시성 이슈 해결 방법 (2) (0) | 2022.09.21 |
[Java] 쓰레드(Thread)의 실행제어 (1) | 2022.09.15 |
[Java] 쓰레드(Thread)의 기초 (1) | 2022.09.14 |
[Java] 열거형(enums) (0) | 2022.09.13 |