
https://www.youtube.com/watch?v=UOWy6zdsD-c&t=424s
토스증권의 영상을 참고하였습니다

| 구분 | 낙관적 락 (Optimistic Lock) | 비관적 락 (Pessimistic Lock) |
|---|---|---|
| 경합 처리 방식 | 트랜잭션 충돌 시 예외 발생 후 재시도 | 트랜잭션이 완료될 때까지 락을 유지 |
| 성능 | 충돌이 적을 때 성능 좋음 | 트랜잭션이 길어질수록 성능 저하 |
| 예제 | @Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT) | @Lock(LockModeType.PESSIMISTIC_WRITE) |
@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT) 사용@Repository
public interface StockRepository extends JpaRepository<Stock, UUID> {
@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT) // 낙관적 락 사용
@Query("SELECT s FROM Stock s WHERE s.product.productId = :productId")
Optional<Stock> findByProduct_ProductIdWithLock(UUID productId);
}
@Lock(OPTIMISTIC_FORCE_INCREMENT)을 사용하여 충돌 감지OptimisticLockException 예외 발생 → 이를 감지하여 자동 재시도 수행@Service
@RequiredArgsConstructor
public class StockRetryService {
private final StockRepository stockRepository;
// 예약 구매: 재고 1 감소 메서드
@Retryable(
retryFor = {StaleObjectStateException.class, OptimisticLockException.class, ObjectOptimisticLockingFailureException.class},
maxAttempts = 500, // 최대 500번 재시도
backoff = @Backoff(100) // 100ms 대기 후 재시도
)
@Transactional
public void decreaseStockQuantityWithRetry(UUID productId) {
Stock stock = stockRepository.findByProduct_ProductIdWithLock(productId)
.orElseThrow(() -> new CustomException(CommerceErrorCode.STOCK_DATA_NOT_FOUND_FOR_PRODUCT));
stock.decreaseStock(); // 수량 1 감소
stockRepository.save(stock);
}
}
@Retryable을 사용하는 이유
OptimisticLockException)가 발생하면 자동으로 재시도@Version을 사용할 때 JPA의 동작 방식 @Version이 있는 엔티티(Stock)에서 업데이트가 발생하면, JPA는 자동으로 WHERE version = ? 조건을 추가함
@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT)
@Query("SELECT s FROM Stock s WHERE s.product.productId = :productId")
Optional<Stock> findByProduct_ProductIdWithLock(UUID productId);
WHERE version = ?을 추가한 UPDATE 쿼리를 실행한다.@Version 필드가 존재하기 때문에 UPDATE 쿼리를 실행할 때 version을 체크하고, 충돌이 발생하면 OptimisticLockException을 발생시킨다.RLock lock = redissonClient.getLock("stock:" + productId);
try {
if (!lock.tryLock(10, 2, TimeUnit.SECONDS)) {
throw new CustomException(CommerceErrorCode.LOCK_ACQUISITION_FAILED);
}
// 낙관적 락과 함께 사용 (버전 증가)
Stock stock = stockRepository.findByProduct_ProductIdWithLock(productId)
.orElseThrow(() -> new CustomException(CommerceErrorCode.STOCK_DATA_NOT_FOUND_FOR_PRODUCT));
stock.decreaseStock(); // 재고 감소
stockRepository.save(stock); // 업데이트 시 WHERE version = ? 자동 추가
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
→ 즉, 멀티 서버 환경 + JPA 동시성 제어 를 동시에 해결하는 방식!