동시성 문제 - Sychronized

김건우·2023년 1월 23일
0
post-thumbnail

동시성 문제란?

동시성 문제란, 동일한 하나의 데이터에 2 이상의 스레드, 혹은 세션에서 가변 데이터를 동시에 제어할 때 나타는 문제로, 하나의 세션이 데이터를 수정 중일때, 다른 세션에서 수정 전의 데이터를 조회해 로직을 처리함으로써 데이터의 정합성이 깨지는 문제를 말합니다.

Stock class

  • 재고를 갖고있는 Stock 클래스
@Entity
@Getter
@NoArgsConstructor
public class Stock {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private Long productId;
    private Long quantity;
    @Version
    private Long version;

    public Stock(final Long id, final Long quantity) {
        this.id = id;
        this.quantity = quantity;
    }

    public void decrease(final Long quantity) {
        if (this.quantity - quantity < 0) {
            throw new RuntimeException("재고 부족");
        }
        this.quantity = this.quantity - quantity;
    }
}

StockService

재고 감소 비지니스로직을 갖고있는 service 레이어

@Service
@RequiredArgsConstructor
public class StockService {

    private final StockRepository stockRepository;

    /**
     * 재고 감소
     */
    @Transactional
    public synchronized void decrease(final Long id, final Long quantity) {
        Stock stock = stockRepository.findById(id).orElseThrow();
        stock.decrease(quantity);
        stockRepository.saveAndFlush(stock);
    }
}

테스트 코드를 만들기 전에 우선 테스트 코드에서 사용되는 인터페이스를 소개하겠습니다.

ExecutorService

  • ExecutorService란, 병렬 작업 시 여러 개의 작업을 효율적으로 처리하기 위해 제공되는 JAVA 라이브러입니다.
  • ExecutorService는 손쉽게 ThreadPool을 구성하고 Task를 실행하고 관리할 수 있는 역할을 합니다.
  • Executors 를 사용하여 ExecutorService 객체를 생성하며, 쓰레드 풀의 개수 및 종류를 지정할 수 있는 메소드를 제공합니다.

CountDownLatch

  • CountDownLatch란, 어떤 스레드가 다른 쓰레드에서 작업이 완료될 때 가지 기다릴 수 있도록 해주는 클래스입니다.
  • CountDownLatch 를 이용하여, 멀티스레드가 100번 작업이 모두 완료한 후, 테스트를 하도록 기다리게 합니다.
  • CountDownLatch 작동원리
  1. new CountDownLatch(5); 를 이용해 Latch할 갯수를 지정합니다.
  2. countDown()을 호출하면 Latch의 숫자가 1개씩 감소합니다.
  3. await() 은 Latch의 숫자가 0이 될 때 까지 기다리는 코드입니다.
@Test
public void 동시에_100개의_요청() throws InterruptedException {
    int threadCount = 100;
    //멀티스레드 이용 ExecutorService : 비동기를 단순하게 처리할 수 있또록 해주는 java api
    ExecutorService executorService = Executors.newFixedThreadPool(32);

    //다른 스레드에서 수행이 완료될 때 까지 대기할 수 있도록 도와주는 API - 요청이 끝날때 까지 기다림
    CountDownLatch latch = new CountDownLatch(threadCount);

    for (int i = 0; i < threadCount; i++) {
        executorService.submit(() -> {
                try {
                    stockService.decrease(1L, 1L);
                }
                finally {
                    latch.countDown();
                }
            }
        );
    }

    latch.await();

    Stock stock = stockRepository.findById(1L).orElseThrow();

    //100 - (1*100) = 0
    assertThat(stock.getQuantity()).isEqualTo(0L);

}
  • 하지만 아래와 같이 테스트에 실패라게 됩니다.(100개의 제고를 -1을 해주어서 0을 예상했습니다)

    테스트가 실패한 이유

  • 그 이유는 레이스 컨디션(Race Condition) 이 일어나기 때문입니다.
  • 레이스 컨디션이란, 2 이상의 스레드가 공유 데이터에 액세스 할 수 있고, 동시에 변경하려할 떄 발생할 수 있는 문제

1.Synchronized 이용

  • Synchronized를 메소드에 명시해주면 하나의 스레드만 접근이 가능합니다.
  • 멀티스레드 환경에서 스레드간 데이터 동기화를 시켜주기 위해서 자바에서 제공하는 키워드 입니다.
    공유되는 데이터의 Thread-safe를 하기 위해, synchronized 로 스레드간 동기화를 시켜 thread-safe 하게 만들어줍니다.
  • 자바에서 지원하는 synchronized는, 현제 데이터를 사용하고 있는 해당 스레드를 제외하고 나머지 스레드들은 데이터 접근을 막아 순차적으로 데이터에 접근할 수 있도록 해줍니다.

Synchronized를 사용한 service

/**
 * 재고 감소
 */
@Transactional
public synchronized void decrease(final Long id, final Long quantity) {
    Stock stock = stockRepository.findById(id).orElseThrow();
    stock.decrease(quantity);
    stockRepository.saveAndFlush(stock);
}
  • 아래와 같이 테스트에 성공🔽

JAVA Sychronized 의 문제점

  • 자바의 Sychronized는 하나의 프로세스 안에서만 보장이 됩니다.
  • 즉, 서버가 1대일때는 문제가 없지만 서버가 2대 이상일 경우 데이터에 대한 접근을 막을 수가 없습니다.

다음 장에서 부터는 DataBase를 이용한 Lock 방법을 소개하겠습니다.

profile
Live the moment for the moment.

0개의 댓글