동시성문제(1) 에서 동시성문제가 무엇이고 왜 일어나는지 알아보았다. 이번 포스트에서는 자바의 synchronized 를 사용하여 동시성 문제를 해결할 수 있는지, 어떻게 적용하는지 알아보겠다.
여러개의 스레드가 한개의 자원을 사용하고자 할 때,
현재 데이터를 사용하고 있는 해당 스레드를 제외하고 나머지 스레드들은 데이터에 접근 할 수 없도록 막는 개념
그렇다면 코드에 적용해보자.
StockService
@Transactional
public synchronized void decrease(Long id, Long quantity) {
Stock stock = stockRepository.findById(id).orElseThrow();
stock.decrease(quantity);
stockRepository.save(stock);
}
StockServiceTest
@Test
public void sameRequest() throws InterruptedException {
int threadCount = 100;
// ExecutorService : 비동기로 실행하는 작업을 단순화하여 사용할 수 있도록 도와주는 자바의 API
ExecutorService executorService = Executors.newFixedThreadPool(32);
// CountDownLatch : 다른 스레드에서 수행중인 작업이 완료될 때 까지 대기할 수 있도록 도와주는 클래스
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i=0; i<100; i++) {
executorService.submit( () -> {
try {
stockService.decrease(1L, 1L);
} finally {
latch.countDown();
}
});
}
latch.await();
Stock stock = stockRepository.findById(1L).orElseThrow();
assertEquals(0, stock.getQuantity());
}
이렇게하고 테스트 코드를 실행시켜보자.
현재 데이터를 사용하고 있는 해당 스레드를 제외하고, 나머지 스레드들은 데이터에 접근 할 수 없도록 막기 때문에 테스트 코드는 정상적으로 통과될 것으로 예상을 하였다.
하지만 테스트 코드를 통과하지 못했다. 이유가 무엇일까?
이유는 Spring 의 @Transaction 동작방식 때문이다. 간단히 코드를 살펴보자.
.
.
트랜잭션 동작 코드(TransactionStockService)
@Service
public class TransactionStockService {
private final StockService stockService;
public void decrease(Long id, Long quantity) {
// 트랜잭션 시작
startTransaction();
stockService.decrease(id, quantity);
// 트랜잭션 종료
endTransaction();
}
private void startTransaction() {
System.out.println("트랜잭션 시작");
}
private void endTransaction() {
System.out.println("트랜잭션 종료");
}
}
살펴볼 코드
// 트랜잭션 시작
startTransaction();
stockService.decrease(id, quantity);
// 트랜잭션 종료
endTransaction();
이 부분을 보자. 트랜잭션이 끝나고 스레드1의 작업(decrease) 이 끝나고 트랜잭션이 commit() 되기 전, 스레드2 가 작업(decrease) 을 실행하고 commit() 을 한다. 이렇게 되면 또 Race Conditon 이 발생하게 된다.
.
.
(트랜잭션의 자세한 내용은 나중에 더 공부해서 올려보겠다😎)
때문에 @Transaction 을 주석처리하고 다시 테스트 코드를 실행해보면
다음과 같이 재고가 원하는 값이 0 으로 잘 처리되는 것을 알 수 있다.
테스트 코드가 정상적으로 동작했으니 @Transaction 을 주석처리하고 synchronized을 사용하여 동시성 문제를 완전히 해결할 수 있는 것 일까? 정답은 '아니다' 이다. 왜그럴까?
synchronized 는 각 프로세스 안에서만 보장이 되기 때문에 결국 여러 스레드에서 동시에 데이터에 접근을 할 수 있게 되면서 레이스 컨디션이 발생하게 된다. 실제로 운영중인 서비스는 거의 2대 이상의 서버를 운영하고 있기 때문에 synchronized 는 거의 사용하지 않게 된다.
때문에 이러한 문제를 해결하기 위해 다음 포스트에서는 MySQL이 지원해주는 방법으로 레이스 컨디션을 해결하는 방법을 알아보겠다.
https://coding-start.tistory.com/68
https://ksh-coding.tistory.com/125