동시성 이슈 해결방법[1] - 재고 감소 로직 작성

Euiyeon Park·2025년 2월 17일
0
post-thumbnail

🛒📦재고시스템으로 알아보는 동시성 이슈 해결 방법

1. 재고 감소 로직 작성

📂Stock.java

@Entity
@Getter
@NoArgsConstructor
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 void decreaseQuantity(Long quantity) {
        if (this.quantity - quantity < 0) {
            throw new RuntimeException("재고는 0개 미만이 될 수 없습니다.");
        }

        this.quantity -= quantity;
    }
}

📂StockService.java

@Service
@Transactional
@RequiredArgsConstructor
public class StockService {

    private final StockRepository stockRepository;

    public void decreaseStock(Long id, Long quantity) {
        // Stock을 조회
        // 재고 감소
        // 갱신된 값을 저장
        Stock stock = stockRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 id입니다."));

        stock.decreaseQuantity(quantity);
        stockRepository.saveAndFlush(stock);
    }
}

📂StockServiceTest.java

@SpringBootTest
class StockServiceTest {

    @Autowired
    private StockService stockService;

    @Autowired
    private StockRepository stockRepository;

    // 테스트 전 상품 재고 입력
    @BeforeEach
    public void beforeTest() {
        stockRepository.saveAndFlush(new Stock(1L, 100L));
    }

    // 테스트 후 모든 재고 삭제
    @AfterEach
    public void afterTest() {
        stockRepository.deleteAll();
    }

    @Test
    @DisplayName("재고 감소 로직 테스트")
    public void decreaseStockTest(){

        // when
        stockService.decreaseStock(1L, 1L);

        // then
        Stock stock  = stockRepository.findById(1L)
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 id"));

        assertAll(
                () -> assertNotNull(stock),
                () -> assertEquals(99, stock.getQuantity())
        );
    }
}

💡 save()

  • 트랜잭션이 끝날 때 까지 영속성 컨텍스트에 저장된 상태로 유지
  • 즉시 데이터베이스에 반영❌, 영속성 컨텍스트가 Flush될 때 반영
  • 트랜잭션이 끝날 때 한꺼번에 반영되므로 성능이 최적화 됨(Batch Insert⭕)

💡 saveAndFlush()

  • 즉시 데이터베이스에 반영(즉시 Flush 발생)
  • 영속성 컨텍스트를 강제로 비우지 않음, 하지만 변경 사항을 DB에 반영함
  • 이후 트랜잭션이 종료될 때 다시 Flush가 발생할 수 있음
  • saveAndFlush()를 불필요하게 사용하면 성능 저하(DB I/O 증가)

2. 재고 감소 로직의 문제점

🚦 Race Condition

  • Race Condition여러 개의 프로세스 또는 스레드가 공유 자원에 동시 접근(변경) 할 때
    발생하는 문제로, 실행 순서에 따라 예상치 못한 결과가 발생하는 상태

재고 감소 예상 동작 과정

Thread1StockThread2
SELECT *
FROM stock
WHERE id = 1
{id: 1, quantity: 5}
UPDATE SET quantity = 4
FROM stock
WHERE id = 1
{id: 1, quantity: 4}
{id: 1, quantity: 4}SELECT *
FROM stock
WHERE id = 1
{id: 1, quantity: 3}UPDATE SET quantity = 4
FROM stock
WHERE id = 1

재고 감소 실제 동작 과정

  • Thread1이 데이터를 가져서 갱신하기 전에 Thread2가 데이터를 가져가서 갱신
  • Thread1Thread2가 모두 갱신을 하지만,
    둘 다 재고가 5인 값을 갱신하므로 갱신이 누락
Thread1StockThread2
SELECT *
FROM stock
WHERE id = 1
{id: 1, quantity: 5}
{id: 1, quantity: 5}SELECT *
FROM stock
WHERE id = 1
UPDATE SET quantity = 4
FROM stock
WHERE id = 1
{id: 1, quantity: 4}
{id: 1, quantity: 4}UPDATE SET quantity = 4
FROM stock
WHERE id = 1

3. 해결 방법

  • 하나의 Thread가 작업이 완료된 이후에, 다른 스레드가 데이터에 접근할 수 있도록 해야 함

ref

재고시스템으로 알아보는 동시성이슈 해결방법

profile
"개발자는 해결사이자 발견자이다✨" - Michael C. Feathers

0개의 댓글