강의
[재고시스템으로 알아보는 동시성 이슈 해결방법] #2 재고감소 로직 작성 및 테스트
dev_rosieposie
2023. 11. 6. 18:11
들어가기 전 ..
1. 이전 게시글과 이어지는 게시글이므로 프로젝트 환경 및 세팅은 아래 링크에서 확인해주세요!
2. 전체 코드는 아래의 Git에서 확인 가능합니다!
참고하면 좋을 이전 글
목표
- 재고감소를 위한 엔티디와 비지니스로직, curd로직을 작성하자
- 해당 재고감소코드를 테스트할 테스트코드를 작성해보자
- 해당 코드가 가지고오는 문제점이 무엇인지 이해하자
재고감소 코드
Stock class
package com.example.stock.domain;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
//entity는 database의 테이블이라고 생각
@Entity
public class Stock {
// id, productId, quantity
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long productId;
private Long quantity;
public Stock() {
}
public Stock(Long productId, Long quantity) {
this.productId = productId;
this.quantity = quantity;
}
//test case를 위한 수량 확인
public Long getQuantity() {
return quantity;
}
// 재고 감소 - 수량
public void decrease(Long quantity){
if(this.quantity - quantity < 0){
throw new RuntimeException("0개 미만");
}
this.quantity = this.quantity-quantity;
}
}
- domain 패키지 밑 위치
- @Entity를 적용하여 JPA를 통해 테이블과 매핑한다. - 기본 생성자 필수
- 재고감소 메소드, 수량 확인을 위한 getQauntity() 메소드
StockService class
@Service
public class StockService {
//stock에 대한 crud가 가능해야하므로, StockRespository를 필드로 가진다
private StockRepository stockRepository;
public StockService(StockRepository stockRepository) {
this.stockRepository = stockRepository;
}
@Transactional
public void decrease(Long id, Long quantity){
/* 1 재고 확인
2 재고 감소
3 갱신 값 저장
*/
Stock stock = stockRepository.findById(id).orElseThrow();
stock.decrease(quantity);
stockRepository.saveAndFlush(stock);
}
}
- 재고감소 메소드
StockRepository class
// 재고에 대한 crud
public interface StockRepository extends JpaRepository<Stock, Long> {
}
- jpa를 상속받은 StockRepository
StockApplicationTests class
@SpringBootTest
class StockApplicationTests {
@Autowired
private StockService stockService;
@Autowired
private StockRepository stockRepository;
// 테스트 전에 데이터베이스에 재고가 있어야 하므로 데이터 생성
@BeforeEach
public void before(){
stockRepository.saveAndFlush(new Stock(1L,100L));
}
// 테스트 후 데이터 삭제
@AfterEach
public void after(){
stockRepository.deleteAll();
}
@Test
public void 재고감소() {
stockService.decrease(1L, 1L);
Stock stock = stockRepository.findById(1L).orElseThrow();
assertEquals(99, stock.getQuantity());
}
}
- @BeforeEach 와 @AfterEach를 통해 데이터베이스에 재고를 생성하고 삭제한다
- @Test 재고감소 코드 작성
- 1L의 아이디를 가진 재고를 1L만큼 감소 시킨다 [ 현재재고 100-1 = 99 (기대값) ]
결과
=> 테스트 통과
왜냐하면, 단일 서버 환경 기준이기 때문 그렇다면 이 코드로 작성했을 때 다중서버에서 문제는?
@Test
public void 동시에_100개의_요청() throws InterruptedException {
// 100개의 요청을 보낼 것임
int threadCount = 100;
// 멀티 스레드를 이용하기 위해 ExecutorService 사용 - (비동기로 실행하는 작업을 단순화하여 사용하게 하는 자바 api)
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch latch = new CountDownLatch(threadCount);
/*
1OO개의 요청이 모두 끝날때까지 기다려야 하므로 CountDownLatch 사용
CountDownLatch는 다른 스레드에서 수행죽인 작업이 완료될 때까지 대기할 수 있도록 도와주는 클래스 */
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*00) = 0 : 예상 값
assertEquals(0, stock.getQuantity());
}
- 다중 서버를 구현하기 위해 멀티스레드 사용
- 100개의 요청
결과
=> 실패. 기대값은 0이었으나 96임
왜 그럴까?
이유 : 레이스 컨디션이 일어났기 때문
레이스 컨디션이란?
둘 이상의 Thread가 공유 데이터에 액세스 할 수 있고 동시에 변경을 하려고할 때 발생하는 문제
예시를 살펴보면,
- 우리는 Thread1이 데이터를 가져가서 갱신을 한 값을 Thread2가 가져간 이후에 갱신을 하는 것을 예상함
- 하지만, 실제로는 Thread1이 데이터를 가져가서 갱신을 하기 이전에 Thread2가 가져가서 갱신되기 전의 값을 가져가게 됨
- 그리고 Thread1이 갱신을 하고 Thread2도 갱신을 하지만 둘다 재고가 5인 상태에서 1을 줄인 값을 갱신하기 때문에 갱신이 누락되게 됨
=> 이렇게 두개 이상의 스레드가 공유 데이터에 엑세스 할 수 있고 동시에 변경을 하려고 할 때 발생하는 문제를 레이스 컨디션이라고 함
그럼 어떻게 해결을 해야할까?
key point ! 하나의 스레드가 작업이 완료된 이후에 다른 스레드가 데이터에 접근할 수 있도록 하게 하면 된다.
문제를 해결할 수 있는 해결 방법은 여러가지가 있고,, 투비 컨티뉴 ...!!
강의 참고