[Spring] πŸš©λ™μ‹œμ„± 이슈 λ°œμƒ μƒν™©πŸš©

πŸ™ˆΒ·2024λ…„ 1μ›” 10일
0
post-thumbnail

μΈν”„λŸ°μ˜ μž¬κ³ μ‹œμŠ€ν…œμœΌλ‘œ μ•Œμ•„λ³΄λŠ” λ™μ‹œμ„±μ΄μŠˆ ν•΄κ²°λ°©λ²•κ°•μ˜λ₯Ό λ“£κ³  μ •λ¦¬ν•œ κΈ€μž…λ‹ˆλ‹€.
κ°•μ˜λ₯Ό λ“€μœΌλ©° λ‚΄μš©κ³Ό μ½”λ“œλŠ” github에 μ •λ¦¬ν•΄λ‘μ—ˆμŠ΅λ‹ˆλ‹€.

기본 둜직

재고 μ‹œμŠ€ν…œμ—μ„œ μž¬κ³ λŸ‰μ„ κ°μ†Œμ‹œν‚€λ©° λ™μ‹œμ„± λ¬Έμ œκ°€ λ°œμƒν•˜λŠ” 상황을 κ°€μ •ν•œλ‹€.

  • Stock 재고λ₯Ό 가진 객체
  • StockRepository JPA μ‚¬μš©μ„ μœ„ν•œ JpaRepository 상속
  • StockService 재고 κ°μ†Œ 둜직

πŸ“ˆ Stock

@Entity
public class Stock {

    @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;
    }

    public Long getQuantity() {
        return quantity;
    }

    // 재고 κ°μ†Œ
    public void decrease(Long quantity) {
        if(this.quantity - quantity < 0) {
            throw new RuntimeException("μž¬κ³ κ°€ λΆ€μ‘±ν•©λ‹ˆλ‹€.");
        }
        this.quantity -= quantity;
    }
}

πŸ“‰ StockService

@Service
public class StockService {

    private final StockRepository stockRepository;

    public StockService(StockRepository stockRepository) {
        this.stockRepository = stockRepository;
    }

    // 재고 κ°μ†Œ
    public void decreaseStock(Long id, Long quantity) {
        // Stock 쑰회
        Stock stock = stockRepository.findById(id).orElseThrow();
        stock.decrease(quantity);
        stockRepository.saveAndFlush(stock);
    }
}

λ™μ‹œμ„± 문제 (Concurrency Issue)λž€,

ν•˜λ‚˜μ˜ 데이터λ₯Ό λ‘˜ μ΄μƒμ˜ threadλ‚˜ session이 μ œμ–΄ν•  λ•Œ λ°œμƒν•˜λŠ” 문제둜

ν•˜λ‚˜μ˜ μ„Έμ…˜μ΄ 데이터λ₯Ό μˆ˜μ • μ€‘μΌλ•Œ, λ‹€λ₯Έ λ°μ΄ν„°μ—μ„œ μˆ˜μ • μ „μ˜ 데이터λ₯Ό μ‘°νšŒν•˜μ—¬ λ‘œμ§μ„ μ²˜λ¦¬ν•˜λ―€λ‘œμ¨ λ°μ΄ν„°μ˜ μ •ν•©μ„œμ΄ κΉ¨μ§€λŠ” 문제λ₯Ό λ§ν•œλ‹€.

✍🏻 예제 μ½”λ“œ

ν…ŒμŠ€νŠΈμ—μ„œ μ‚¬μš©λ˜λŠ” 라이브러리/ν΄λž˜μŠ€λŠ”

  • ExecutorService
    λΉ„λ™κΈ°λ‘œ μ‹€ν–‰ν•˜λŠ” μž‘μ—…μ„ λ‹¨μˆœν™”ν•˜μ—¬ μ‚¬μš©ν•  수 μžˆλ„λ‘ λ„μ™€μ£ΌλŠ” Java API이닀.
  • CountDownLatch
    λ‹€λ₯Έ μŠ€λ ˆλ“œκ°€ μˆ˜ν–‰ν•˜λŠ” μž‘μ—…μ΄ 끝날 λ•ŒκΉŒμ§€ 기닀릴 수 μžˆλŠ” κΈ°λŠ₯을 μ œκ³΅ν•œλ‹€.
@SpringBootTest
public class StockServiceTest {

    @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.decreaseStock(1L, 1L);

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

        assertEquals(99L, stock.getQuantity()); // 100개 - 1개 = 99개
    }

    @Test
    public void λ™μ‹œμ—_100개_μš”μ²­() throws InterruptedException {
        int threadCount = 100;
        ExecutorService executorService = Executors.newFixedThreadPool(32);
        CountDownLatch latch = new CountDownLatch(threadCount);

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

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

        assertEquals(0L, stock.getQuantity());
    }

}

μœ„ ν…ŒμŠ€νŠΈ μ½”λ“œμ—μ„œ μž¬κ³ κ°μ†Œ()λŠ” μ„±κ³΅μ μœΌλ‘œ μ‹€ν–‰λ˜μ§€λ§Œ λ™μ‹œμ—_100개_μš”μ²­()은 μ‹€νŒ¨ν•œλ‹€.

κ·Έ μ΄μœ λŠ” Race condition이 λ°œμƒν–ˆκΈ° λ•Œλ¬Έμ΄λ‹€.
Race condition은 λ‘˜ μ΄μƒμ˜ threadκ°€ 곡유 μžμ›μ— λ™μ‹œμ— μ ‘κ·Όν•˜μ—¬ λ³€κ²½ν•˜λ €κ³  ν•  λ•Œ λ°œμƒν•˜λŠ” λ¬Έμ œμ΄λ‹€.

❓ λ©€ν‹°μŠ€λ ˆλ“œ ν™˜κ²½μ—μ„œ race condition λ°œμƒ 이유

κΈ°λŒ€ν–ˆλ˜ μˆœμ„œλŠ”
Thread1 재고 확인 πŸ ’ Thread1 재고 κ°μ†Œ πŸ ’ Thread2 재고 확인 πŸ ’ Thread2 재고 κ°μ†Œ πŸ ’ ... μ΄μ§€λ§Œ

μ‹€μ œ μ‹€ν–‰ μˆœμ„œλŠ”
Thread1 재고 확인 πŸ ’ Thread2 재고 확인 πŸ ’ Thread1 재고 κ°μ†Œ πŸ ’ Thread2 재고 κ°μ†Œ πŸ ’ ... μ΄λ―€λ‘œ λ¬Έμ œκ°€ λ°œμƒν•œλ‹€.

πŸ™Œ race condition ν•΄κ²°ν•˜κΈ°

race condition 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ ν•˜λ‚˜μ˜ μŠ€λ ˆλ“œ μž‘μ—…μ΄ μ™„λ£Œλœ 이후에 λ‹€λ₯Έ μŠ€λ ˆλ“œ μž‘μ—…μ„ ν•˜λ„λ‘ν•œλ‹€.

κ°•μ˜μ—μ„œ μ†Œκ°œν•˜λŠ” λ°©λ²•μœΌλ‘œλŠ” 크게 3가지가 μžˆλ‹€.

  • Application levelμ—μ„œμ˜ ν•΄κ²° λ°©μ•ˆ
    • Java의 synchronized
  • Database의 Lock ν™œμš©
    • Pessimistic lock(비관적 락)
    • Optimistic lock(낙관적 락)
    • Named lock
  • Redis Distributed Lock ν™œμš©
    • Lettuce 라이브러리
    • Redisson 라이브러리
profile
개발 일기🌱

0개의 λŒ“κΈ€