[Spring] 동시성 문제(2) - synchronized

Kyungmin·2024년 6월 19일
0

Spring

목록 보기
23/39

동시성문제(1) 에서 동시성문제가 무엇이고 왜 일어나는지 알아보았다. 이번 포스트에서는 자바의 synchronized 를 사용하여 동시성 문제를 해결할 수 있는지, 어떻게 적용하는지 알아보겠다.

synchronized

여러개의 스레드가 한개의 자원을 사용하고자 할 때,
현재 데이터를 사용하고 있는 해당 스레드를 제외하고 나머지 스레드들은 데이터에 접근 할 수 없도록 막는 개념

그렇다면 코드에 적용해보자.

StockService

  • 아래 코드에서와 같이 메소드 선언부에 synchronized 를 선언해준다.
@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 으로 잘 처리되는 것을 알 수 있다.

그렇다면 synchronized로 동시성을 완전히 해결?

테스트 코드가 정상적으로 동작했으니 @Transaction 을 주석처리하고 synchronized을 사용하여 동시성 문제를 완전히 해결할 수 있는 것 일까? 정답은 '아니다' 이다. 왜그럴까?

synchronized 문제점

  • 자바의 synchronized 는 하나의 프로세스 안에서만 보장이 된다. 서버가 1대일 때는 데이터의 접근을 서버가 1대만 상대하기 때문에 괜찮겠지만, 서버가 2대 이상일 때는 데이터의 접근을 여러 서버에서 가능하게 된다. 때문에 서버가 2개 이상이면 또다시 레이스 컨디션이 발생하게 된다.

synchronized 는 각 프로세스 안에서만 보장이 되기 때문에 결국 여러 스레드에서 동시에 데이터에 접근을 할 수 있게 되면서 레이스 컨디션이 발생하게 된다. 실제로 운영중인 서비스는 거의 2대 이상의 서버를 운영하고 있기 때문에 synchronized 는 거의 사용하지 않게 된다.

때문에 이러한 문제를 해결하기 위해 다음 포스트에서는 MySQL이 지원해주는 방법으로 레이스 컨디션을 해결하는 방법을 알아보겠다.


참고

https://coding-start.tistory.com/68
https://ksh-coding.tistory.com/125

profile
Backend Developer

0개의 댓글

관련 채용 정보