<재고시스템으로 알아보는 동시성이슈 해결방법>을 참고해서 코드를 구현했다.
여러 스레드가 하나의 공유변수에 접근해서 변경할 때 그 데이터에 정합성 문제가 생길 수 있다.
스레드1이 변수 1000원이었던 money 변수 값에 5000원을 더하고, 스레드2가 3000원을 더한다면 money의 최종값은 9000원이어야 한다.
하지만, 동시에 데이터를 읽고서 변경한다면
9000원이 아닌 6000원 혹은 4000원이 될 수도 있다.
스레드1과 스레드2가 값을 동시에 읽은 뒤, 스레드 1이 값을 변경하고 스레드2가 값을 변경할 수 있어서다.
public void decrease(Long quantity){
if(this.quantity -quantity<0){
throw new RuntimeException("재고는 0개 미만이 될 수 없습니다.");
}
this.quantity -= quantity;
}
이렇게 수량을 감소시키는 로직이 있다고 해보자.
@Test
public void 동시에_100개의_요청() throws InterruptedException {
int threadCount =100;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for(int i= 0; i<threadCount; i++){
executorService.submit(()->{
try{
service.decrease(1L, 1L);
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await();
Stock stock = repository.findByProductId(1L);
assertEquals(0, stock.getQuantity());
}
동시성 이슈가 없는 세계에서는 위 테스트가 통과해야 한다.
하지만, 재고가 50, 60, 70 이렇게 남아 있다.
동시성 이슈 탓이다.
간단한 방법으로는
public synchronized void decrease(Long id, Long quantity){
Stock stock = stockRepository.findById(id).orElseThrow();
stock.decrease(quantity);
stockRepository.saveAndFlush(stock);
}
synchronized를 메서드에 붙여줄 수 있다.
하나의 스레드만 메서드에 접근하도록 락을 걸어주는 방법이다. 다만, 이 방법은 성능을 저하시킬 수 있다.
참고로
@Transctional을 걸어주면 위 방법으로도 해결이 안된다.
스프링이 @Transctional이 걸려 있는 클래스를 새로 만드는데(stockService를 필드로 갖는 클래스) 메서드를 실행->트랜잭션 종료하는 과정에 업데이트를 하는 방식이다.
그 중간에 다른 스레드가 접근해서 값을 가져오고 변경할 수 있다고 한다.
트랜잭션이 메서드 위에 걸려 있으면 실제로 테스트가 실패했다.
강의에 의하면, synchronize는 하나의 프로세스에서만 락이 보장된다.
서버가 두개 이상이어서 여러 군데에서 데이터에 접근하면 동시성 문제가 발생하게 된다.
이에 대해서 비관적 락을 사용하는 방법이 있다.
이는 db단에서 락을 거는 방식이라고 한다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select s from Stock s where s.productId = :productId")
Stock findByProductIdWithPessimisticLock(Long productId);
Stock findByProductId(Long productId);
이렇게 레포지토리에서 직접 메서드를 만들어주면 끝.
데이터에 락을 거는 방식으로 방어를 하는데, 서버 1이 락을 걸고 데이터를 가져가면, 다른 서버에서 그 데이터에 접근하지 못한다.
문제는 데드락이 생길 수 있다는 점이다.
충돌이 빈번한 경우에는 optimistic lock보다 성능이 좋을 수 있다.
테스트가 실제로 통과했다!
나머지 강의를 보면서 뒷부분을 채워넣자!