이커머스 프로젝트를 진행했는데 트러블슈팅 내역을 정리해보려고 한다.
먼저 상품의 재고처리에 대한 동시성 이슈이다.
현재 상품 주문과 주문취소 시 상품의 재고변경로직이 동작하는데 여기서 동시성 이슈가 발생한다.
1. ❓동시성 이슈란❓
- 여러 스레드가 동시에 같은 인스턴스의 필드 값을 변경하면서 발생하는 문제
- 여러 스레드가 하나의 자원을 공유하고 있기 때문에 같은 자원을 두고 경쟁상태(Race Condition)가 됨
2. 코드와 테스트
코드는 간단히 아래와 같이 구성되어 있다.
// Product.java
@Entity
public class Product extends BaseEntity {
// ... 기타 필드
@Column(nullable = false)
private Long stock;
private Long sales;
public void updateStockAndSalesByOrder(Long quantity) {
this.stock -= quantity;
this.sales += quantity;
}
// .. 기타 로직
}
//ProductService.java
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
// ... 기타 로직
@Transactional
public void updateStockAndSalesByOrder(Product product, Long quantity){
product.updateStockAndSalesByOrder(quantity);
}
// ... 기타 로직
}
정말 멀티스레드 환경에서 재고변경로직을 테스트해보기 위한 테스트코드이다.
@Test
@DisplayName("주문에 의한 재고 업데이트 동시성 이슈")
void testUpdateStockAndSalesByOrderSuccess() throws InterruptedException{
int threadCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(32); // ThreadPool 구성
CountDownLatch latch = new CountDownLatch(threadCount); // 다른 스레드에서 작업이 완료될 때까지 대기
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
try {
productService.updateStockAndSalesByOrder(product.getId(), 1L);
}
finally {
latch.countDown(); //countDown()호출 시 Latch의 숫자가 1씩 감소
}
}
);
}
latch.await(); // Latch의 숫자가 0이 될때까지 기다림
Product newProduct = productService.findById(product.getId()).orElse(null);
assertThat(newProduct).isNotNull();
assertThat(newProduct.getStock()).isZero();
assertThat(newProduct.getSales()).isEqualTo(100L);
}
- ExecutorService : 스레드풀 구성, Executors 를 사용하여 ExecutorService 객체를 생성
- CountDownLatch : 멀티스레드가 100번 작업이 모두 완료한 후, 테스트를 하도록 기다리게 함.
테스트 실패
멀티스레드 환경을 구현한 테스트케이스에서 100개의 재고를 가진 상품에 대해 테스트했을 때 100번의 재고 감소 작업이 이루어진 후에 재고가 0이 되어야하지만 아래와 같이 60개로 조회되는 문제가 발생한다. 레이스 컨디션 때문이다..!
3. 멀티스레드 환경에서 레이스 컨디션 일어나는 이유
스레드A와 스레드B에서 동시에 재고변경로직이 동작할 때 기대하는 동작은 왼쪽과 같이 순차적으로 로직이 동작하는 것이다.
하지만 실제로는 같은 데이터에 대해 동시에 변경로직이 작동하며 오른쪽과 같이 동작한다.
즉, 레이스 컨디션을 해결하기 위해서는 하나의 스레드만 접근 가능하도록 해야한다 ❗
해결방법 1. Synchronized 이용
- 어플리케이션 레벨에서 메소드에 synchronized를 명시해주어 해결하는 방법이다
- synchronized는 현재 데이터를 사용하고 있는 스레드를 제외한 나머지 스레드들의 접근을 막아 순차적으로 데이터에 접근할 수 있도록 해준다.
@Transactional
public synchronized void updateStockAndSalesByOrder(Product product, Long quantity){
product.updateStockAndSalesByOrder(quantity);
productRepository.saveAndFlush(product);
}
문제점
- Sychronized는 하나의 프로세스 안에서만 보장되기 때문에 단일서버에서만 적용 가능하다
- 서버가 여러개라면 문제가 발생한다
- synchronized는 각 인스턴스 안에서만 thread-safe이 보장되기 때문에 분할 서버일 때는 레이스 컨디션 발생
- 실제 서비스는 거의 분할 서버이기 때문에 synchronized만으로는 동시성 이슈를 해결할 수 없다
데이터베이스 락을 이용하여 분할서버에서도 동시성 이슈를 제어할 수 있다.
다음 글에 이어서!
참고
'트러블슈팅' 카테고리의 다른 글
[Spring] Redis 캐시 적용하기 (2/2) (0) | 2023.10.22 |
---|---|
[Spring] Redis 캐시 적용하기 (1/2) (1) | 2023.10.19 |
[Spring] Redis를 통한 세션 불일치 문제 해결 (2/2) (0) | 2023.10.14 |
[Spring] Redis를 통한 세션 불일치 문제 해결 (1/2) (0) | 2023.10.12 |
[Spring] 재고처리 동시성 이슈 해결하기 (2/2) (0) | 2023.07.22 |