Spring에서의 Race Condition을 synchronized로 해결할 수 있을까?

MinSeong Kang·2022년 10월 15일
1

spring

목록 보기
18/18

멀티 쓰레드 환경에서 Java는 Race Condition을 해결하기 위해 synchronized 키워드를 제공한다.

synchronized 키워드가 붙은 메서드 블럭은 하나의 스레드만 접근할 수 있도록 하여 Race Condition이 발생하지 않도록 동작하게 한다.

public class synchronizedTest {

    private static Long count = 0L;

    @AfterEach
    void after() {
        count = 0L;
    }

    public void increase() {
        this.count += 1L;
    }

    public synchronized void synchronized_increase() {
        this.count += 1L;
    }

    @Test
    public void non_synchronized_test() {
        ExecutorService service = Executors.newCachedThreadPool();
        CountDownLatch latch = new CountDownLatch(100);
        for (int i = 0; i < 100; i++) {
            service.submit(() -> {
                try {
                    increase();
                } finally{
                latch.countDown();
            }
            });
        }

        assertThat(count).isEqualTo(100);
    }

    @Test
    public void synchronized_test() throws InterruptedException {
        ExecutorService service = Executors.newCachedThreadPool();

        CountDownLatch latch = new CountDownLatch(100);

        for (int i = 0; i < 100; i++) {
            service.submit(() -> {
                try {
                    synchronized_increase();
                } finally {
                latch.countDown();
            }
            });
        }
        latch.await();
        assertThat(count).isEqualTo(100);
    }
}
  • ExecutorService는 비동기로 실행하는 작업을 도와주는 JAVA API이다.
  • CountDownLatch는 다른 쓰레드의 작업이 완료될 때까지 대기할 수 있도록 도와주는 클래스이다.

위와 같이 synchronized 키워드를 사용하는 것과 사용하지 않는 것을 간단히 테스트해보았을 때, synchronized 키워드를 사용하였을 때 Race Condition이 발생하지 않아 테스트가 성공하는 것을 알 수 있다.


그렇다면, Spring에서 @Transactional을 붙인 메서드에 대해서도 synchronized 키워드를 붙이므로 Race Condition을 해결할 수 있을까??

@Transactional
public synchronized void decrease(Long id, Long quantity) {

    Stock stock = stockRepository.findById(id).orElseThrow(RuntimeException::new);
    stock.decrease(quantity);
}

결론을 말하지만, Race Condition이 발생한다. 왜냐하면 @Transactional의 동작 원리때문이다.
@Transactional을 통한 선언적 트랜잭션 관리 방식은 기본적으로 프록시 방식의 AOP가 적용된다. 트랜잭션 프록시가 트랜잭션 처리 로직을 수행하고 트랜잭션이 시작한 후에 실제 서비스를 대신 호출한다. 이후 트랜잭션을 종료한다.

아래 코드는 트랜잭션 프록시의 간단한 예제 코드이다.

TransactionStatus status = transactionManager.getTransaction(..);

try {
	target.logic(); // public synchronized void decrease 메서드 수행
    // logic 수행 이후 트랜잭션 종료 전에 다른 쓰레드가 decrease에 접근!
    transactionManager.commit(status);
} catch (Exception e) {
	transactionManager.rollback(status);
    throw new IllegalStateException(e); 
}

따라서 한 쓰레드가 synchronized가 붙은 메서드를 호출하여 수행한 이후, 트랜잭션이 종료되기 전에 다른 쓰레드가 synchronized가 붙은 메서드에 접근을 하여 커밋되지 않는 데이터에 접근하게 되는 것이다. 따라서 @Transactional과 synchronized를 함께 사용해서는 우리가 원하는 결과를 얻을 수 없다.

또한 서버가 여러대 있다고 가정하면, synchronized를 사용으로는 데이터베이스의 데이터의 정합성이 맞지 않는 문제를 해결할 수 없다.!!
따라서 해당 문제는 데이터베이스의 락을 통해 Race Condition 문제를 해결해야한다.!

데이터 베이스 락을 통해 Race Condition을 해결하는 방법은 다음 포스팅에서 알아보자 ~~

0개의 댓글