(1) 동시성 이슈 - synchronized

alsdl0629·2023년 12월 14일
0

동시성 이슈

목록 보기
1/4
post-thumbnail

이번 글에서는 최상용님의 "재고시스템으로 알아보는 동시성이슈 해결방법"을 정리해 보려고 합니다.

실습한 레포지토리 👈

재고 감소 기능

@Entity
public class Stock {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long productId;

    private Long quantity;
    
    ...

    public void decrease(long quantity) {
        if (this.quantity - quantity < 0) {
            throw new IllegalArgumentException("재고는 0개 미만이 될 수 없습니다.");
        }

        this.quantity -= quantity;
    }
}
@Transactional
public void decrease(Long id, Long quantity) {
 // 재고 조회
    Stock stock = stockRepository.findById(id).orElseThrow();

    // 재고 감소
    stock.decrease(quantity);
}

재고 감소 기능을 만들고, 잘 작동하는지 테스트 해보겠습니다.

@BeforeEach
void setUp() {
    stockRepository.saveAndFlush(new Stock(1L, 100L));
}
    
@Test
void 동시에_100개의_요청() throws InterruptedException {
    int threadCount = 100;
    // ExecutorService는 비동기로 실행하는 작업을 단순화하여 사용할 수 있게 도와주는 Java API
    ExecutorService executorService = Executors.newFixedThreadPool(32);

    /**
     * 100개의 요청이 모두 끝날 때까지 기다려야 하므로 CountDownLatch 활용
     * 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());
}

재고 100개를 저장하고, 100개의 요청을 보내 재고가 0이 되는지 확인하는 테스트 코드입니다.
Executors, CountDownLatch를 사용해 비동기로 동작하도록 했습니다.

성공할 줄 알았지만, 예상과 반대로 재고가 남아 테스트에 실패했습니다.


문제점

레이스 컨디션이 발생했기 때문에 실패했습니다.

레이스 컨디션이란?
멀티스레드 환경에서 레이스 컨디션은 둘 이상의 Thread공유 데이터에 엑세스할 수 있고, 동시에 변경을 하려고 할 때 발생하는 문제입니다.

Thread-1이 데이터를 가져가 갱신을 하기 이전에 Thread-2가 가져가서 데이터 갱신이 누락되는 문제가 발생했습니다.


문제 해결 방법

하나의 스레드에서 작업이 완료된 이후에 다른 스레드가 데이터에 접근할 수 있도록 해야 합니다.

Java & Spring 에서는 다음과 같은 기술로 레이스 컨디션을 해결할 수 있습니다.

  • Java의 synchronized
  • Database Lock
  • Redis

Java의 synchronized

public synchronized void decrease(Long id, Long quantity) {
    // Stokc 조회
    Stock stock = stockRepository.findById(id).orElseThrow();

    // 재고 감소
    stock.decrease(quantity);

    // 갱신된 값 저장
    stockRepository.save(stock);
}

synchronnized를 활용하면 한 개의 스레드만 접근이 가능하도록 할 수 있습니다.

주의할점

synchronnized를 사용한 함수에서 @Transactional을 사용하면 레이스 컨디션이 발생합니다.

@Transactional트랜잭션 종료 시점에 데이터베이스 업데이트를 하게 됩니다.
실제 데이터베이스가 업데이트 되기 전에 다른 Thread가 decrease()를 호출하면
갱신되기 전에 값을 가져가서 레이스 컨디션이 또 다시 발생하게 됩니다.


그리고 synchronnized하나의 프로세스 안에서만 보장됩니다.
만약 서버가 2대 이상이라면 데이터 접근을 여러 대에서 할 수 있기 때문에
이렇게 되면 또 다시 레이스 컨디션이 발생하게 됩니다.

그래서 실제 운영 중인 서비스에서는 synchronnized를 사용하지 않는다고 합니다.

profile
인풋보다 아웃풋

0개의 댓글