이 포스트는 인프런 최상용 - 재고시스템으로 알아보는 동시성이슈 해결방법 을 참고하여 작성되었습니다.
하나의 자원을 두 개 이상의 스레드가 동시에 제어할 경우 발생할 수 있는 문제입니다.
동시성 이슈에 대한 가장 기본적인 예시로 은행의 입출금 시스템을 많이 보셨을겁니다.
출처: https://yeonyeon.tistory.com/291
위 그림과 같이 서로 다른 A와 B가 100,000원이 들어있는 같은 계좌에 접근하여 입출금 하는 경우를 생각해 볼 수 있습니다.
A는 계좌에서 40,000원을 출금하였고, B는 20,000원을 출금하였습니다.
남은 금액은 40,000원이 되어야 하지만 B에게 보여지는 최종 금액은 80,000원입니다.
이와 같은 상황이 같은 자원을 서로 다른 두 개의 스레드가 점유하여 발생한
동시성 문제 상황의 대표적인 예 입니다.
@Entity
public class Stock {
//고유번호
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
//물품 고유번호
private Long productId;
//수량
private Long quantity;
//Constructor
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 IllegalStateException("재고가 부족합니다.");
}
this.quantity -= quantity;
}
}
@Service
public class StockService{
private StockRepository stockRepository;
public StockService(StockRepository stockRepository){
this.stockRepository = stockRepository;
}
@Transactional
public void decrease(Long id, Long quantity){
Stock stock = stockRepository.findById(id).orElseThrow();
stock.decrease(quantity);
stockRepository.saveAndFlush(stock);
}
}
@Repository
public interface StockRepository extends JpaRepository<Stock, Long> {
}
시나리오
- 재고가 100개인 새로운 Stock 객체를 생성합니다.
- 2개 이상의 스레드를 생성하여 StockService의 decrease()를 호출합니다.
- 호출 과정을 반복 후, 남은 재고의 수와 decrease() 호출 수를 비교합니다.
@SpringBootTest
public class StockTest {
@Autowired
StockService service;
@Autowired
StockRepository repository;
@BeforeEach //테스트 수행에 필요한 데이터 입력
public void beforeTest(){
Stock stock = new Stock(1L, 100);
repository.saveAndFlush(stock);
}
@AfterEach //테스트 종료 후 데이터 삭제
public void afterTest(){
repository.deleteAll();
}
@Test //100개의 서로 다른 스레드를 생성하여 재고 감소 요청 수행
public void concurrency_request_test() 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 {
service.decrease(1L, 1L);
}finally {
latch.countDown();
}
});
}
latch.await();
Stock stock = repository.findById(1L).orElseThrow();
System.out.println("result quantity = " + stock.getQuantity());
}
}