강의의 예제를 통해 배워보려고 한다.
mySQL이 깔려 있어야 하는 게 전제 조건이다.
Stock Entity
@Entity
public class Stock {
// id, productId, quantity
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long productId;
private Long quantity;
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("error");
}
this.quantity = this.quantity - quantity;
}
}
설명
- 식별할 id , 제품 id, 수량으로 구성된다.
- 동시성을 경험해볼 것이기 때문에 Entity를 직접 사용한다
- decrease 메서드를 통해 제품의 수량을 감소시킬 수 있다
StockRepository
public interface StockRepository extends JpaRepository<Stock, Long> {}
StockService
@Service
public class StockService {
@Autowired
private StockRepository stockRepository;
public void decrease(Long id, Long quantity){
Stock stock = stockRepository.findById(id).orElseThrow();
stock.decrease(quantity);
stockRepository.saveAndFlush(stock);
}
}
설명
- 재고의 수량을 감소시키는 decrease 메서드이다
- save()를 사용하지 않고 saveAndFlush() 메서드를 사용한 이유는 바로 DB에 반영하기 위함이다
StockServiceTest
@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);
Stock stock = stockRepository.findById(1L).orElseThrow();
assertThat(stock.getQuantity()).isEqualTo(99L);
}
@Test
public void 동시에_100개씩_요청() throws InterruptedException {
int threadCount = 100;
// 멀티 스레드를 이용하는 자바의 API 비동기
ExecutorService executorService = Executors.newFixedThreadPool(32);
// 100개의 요청이 끝날 때까지 기다려야 하므로
// 다른 스레드에서 수행중인 작업이 완료될 때까지 대기할 수 있도록 도와주는 클래스
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();
assertThat(stock.getQuantity()).isEqualTo(0);
}
}
설명
- beforeEach를 통해 1L, 100L의 stock객체가 하나 저장되어 있게 된다.
- 당연히 100번의 감소를 동시에 실행하면? 0L이 남아야 할 것 같다.
- 예상과 다른 결과가 나온 이유는 Race Condition이 일어났기 때문이다
- Race Condition이란 ? 둘 이상의 스레드가 공유 데이터에 접근할 수 있고, 동시에 변경을 하려고할 때 발생하는 문제
Thread - 1 | Stock | Thread - 2 |
select * from stock where id = 1 |
{id : 1, quantity : 5} | |
update set quantity = 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 quantity = 3 from stock where id = 1 |
- 스레드 1이 데이터를 가져가서 갱신한 값을 스레드 2가 가져와서 갱신을 하는 것을 예상
- 그러나, 실제로는 아래 표
Thread - 1 | Stock | Thread - 2 |
select * from stock where id = 1 |
{id : 1, quantity : 5} | |
{id : 1, quantity : 5} | select * from stock where id = 1 |
|
update set quantity = 4 from stock where id = 1 |
{id : 1, quantity : 4} | |
{id : 1, quantity : 4} | update set quantity = 4 from stock where id = 1 |
- 스레드 1이 데이터를 가져가서 갱신하기 전에 스레드 2가 값을 가져간다
- 그리고 스레드 1이 갱신을 하고 스레드 2도 갱신을 하지만 둘다 재고가 5인 상태에서 1을 줄인 값을 갱신하기 때문에 갱신이 누락되게 된다
- 이렇게 두개 이상의 스레드가 공유 데이터에 접근할 수 있고 동시에 변경하려고할 때 발생하는 문제를 Race Condition이라고 한다
- 이런 문제를 해결하기 위해서는 하나의 스레드가 작업이 완료된 후에 다른 스레드가 데이터에 접근할 수 있도록 하면된다
'CS지식들 > 공부공부' 카테고리의 다른 글
동시성 문제 (3) (0) | 2022.12.19 |
---|---|
동시성 문제 (2) (0) | 2022.12.19 |
JPA에서 페이징/정렬 처리하기 (0) | 2022.11.30 |
Mybatis와 스프링에서 페이징 처리 (0) | 2022.11.30 |
페이징 처리 (오라클) (0) | 2022.11.29 |