728x90
여러 사용자가 동시에 재고를 감소시킬 때, 재고 수량이 올바르게 감소하지 않거나, 재고가 음수가 되는 등의 문제가 발생할 수 있습니다. 이번 포스팅에서는 스프링 부트와 JPA를 사용하여 재고 감소 기능을 구현하고, 동시성 문제를 해결하는 방법과 이를 테스트하는 방법을 소개합니다.
1. 재고(Stock) 엔티티
@Entity
@Getter
public class Stock {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long productId;
private Long quantity;
public Stock(Long productId, Long quantity) {
this.productId = productId;
this.quantity = quantity;
}
public Stock() {
}
public void decrease(Long quantity) {
if (this.quantity - quantity < 0) {
throw new RuntimeException("재고는 0개 미만이 될 수 없습니다.");
}
this.quantity -= quantity;
}
}
2. 재고 서비스
@Service
@RequiredArgsConstructor
public class StockService {
private final StockRepository stockRepository;
public void decrease(Long id, Long quantity) {
// Stock조회
// 재고를 감소 진행
// 감소된 값을 저장
Stock stock = stockRepository.findById(id).orElseThrow();
stock.decrease(quantity);
stockRepository.saveAndFlush(stock);
}
}
3. 재고 레포지토리
@Repository
public interface StockRepository extends JpaRepository<Stock, Long> {
}
4. 동시성 테스트
재고 감소 기능이 여러 스레드에서 동시에 호출될 때 동작을 확인하는 테스트 코드를 작성합니다.
@SpringBootTest
class StockServiceTest {
@Autowired
private StockService stockService;
@Autowired
private StockRepository stockRepository;
// 각 테스트 코드는 독립적으로 존재
// 다른 테스트 영향을 주면 X
// 독립적으로
@BeforeEach
public void createStock() {
stockRepository.saveAndFlush(new Stock(1L, 100L));
}
@AfterEach
public void deleteDb() {
stockRepository.deleteAll();
}
@Test
void 재고감소() {
// given BeforeEach 100개를 생성
// when
stockService.decrease(1L, 1L);
Stock stock = stockRepository.findById(1L).orElseThrow();
Assertions.assertThat(stock.getQuantity()).isEqualTo(99);
// then
}
@Test
void 동시에_100개의_요청() throws InterruptedException {
int threadCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
try {
stockService.decrease(1L, 1L);
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await();
Stock stock = stockRepository.findById(1L).orElseThrow();
Assertions.assertThat(stock.getQuantity()).isEqualTo(0);
}
}
차이가 발생한 걸 확인 할 수 있다.
5. 동시성 문제 해결
여기서 데이터베이스의 Pessimistic Lock을 사용하는 방법을 소개합니다.
Pessimistic Lock 적용
재고 레파지토리
@Repository
public interface StockRepository extends JpaRepository<Stock, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT s FROM Stock s WHERE s.id = :id")
Stock findByIdWithPessimisticLock(Long id);
}
PessimisticLockStockService
@Service
@RequiredArgsConstructor
public class PessimisticLockStockService {
private final StockRepository stockRepository;
@Transactional
public void decrease(Long id, Long quantity) {
Stock stock = stockRepository.findByIdWithPessimisticLock(id);
stock.decrease(quantity);
stockRepository.save(stock);
}
}
추가 작성한 뒤 Test 코드에서 StockService -> PessimisticLockStockService 로 변경하여 주고
다시 테스트를 실행하면, 동시성 문제 없이 재고 감소가 올바르게 작동하는 것을 확인할 수 있습니다.
728x90
'IT 개발 관련 > [프로젝트]' 카테고리의 다른 글
[예약 구매] 자동화 테스트 툴 (0) | 2024.07.27 |
---|---|
[예약구매] 상품 상세 조회 캐싱 적용 전과 후 (1) | 2024.07.22 |
MySQL transaction_isolation 문제 (0) | 2024.06.21 |
[예약구매] API 문서 작성 (0) | 2024.06.20 |
[예약구매] Docker Compose 로컬 개발 환경 구축 (0) | 2024.06.20 |