[기타] 동시성 이슈 01

KIM Jongwan·2023년 7월 5일
0

이 포스트는 인프런 최상용 - 재고시스템으로 알아보는 동시성이슈 해결방법 을 참고하여 작성되었습니다.

동시성 이슈란

하나의 자원을 두 개 이상의 스레드가 동시에 제어할 경우 발생할 수 있는 문제입니다.

동시성 이슈에 대한 가장 기본적인 예시로 은행의 입출금 시스템을 많이 보셨을겁니다.


출처: https://yeonyeon.tistory.com/291

위 그림과 같이 서로 다른 A와 B가 100,000원이 들어있는 같은 계좌에 접근하여 입출금 하는 경우를 생각해 볼 수 있습니다.
A는 계좌에서 40,000원을 출금하였고, B는 20,000원을 출금하였습니다.
남은 금액은 40,000원이 되어야 하지만 B에게 보여지는 최종 금액은 80,000원입니다.


이와 같은 상황이 같은 자원을 서로 다른 두 개의 스레드가 점유하여 발생한
동시성 문제 상황의 대표적인 예 입니다.


코드로 보기

  • Stock.java
    재고 관리를 위한 Entity Class
@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;
    }

}
  • StrockService.java
@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);
    }
}
  • StockRepository.java
@Repository
public interface StockRepository extends JpaRepository<Stock, Long> {

}

테스트 코드

시나리오

  • 재고가 100개인 새로운 Stock 객체를 생성합니다.
  • 2개 이상의 스레드를 생성하여 StockService의 decrease()를 호출합니다.
  • 호출 과정을 반복 후, 남은 재고의 수와 decrease() 호출 수를 비교합니다.
  • StockTest.java
@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());
    }

	
}
profile
2년차 백앤드 개발자입니다.

0개의 댓글