Projects/HubEleven

[동시성 처리] DB Lock : Optimistic Lock (낙관적 락) - 실습

annovation 2026. 2. 20. 23:29

Optimistic Lock (낙관적 락)

💡Optimistic Lock (낙관적 락) 이란?

https://annovation.tistory.com/509

 

[동시성 처리] 동시성 문제 (Race Condition) 해결 방법 3가지 관점 (실습중..)

동시성 문제 (Race Condition)💡해결 방법레이스 컨디션을 해결하는 방법에는 크게 3가지 관점이 있다.첫번째는 Java에서 지원하는 Synchronized 방법으로 해결하기두번째는 DB가 제공하는 Lock 을 이용하

annovation.tistory.com


동시성 처리

💡동시성 처리 전 코드

  • StockServiceImpl.decreaseStock
@Override
@Transactional
public StockResult decreaseStock(StockRequests.Decrease request) {

    Product product = getProductOrThrow(request.productId());

    Stock stock = getStockOrThrow(request.productId());

    stock.decreaseQuantity(request.quantity());

    return StockResult.from(stock, product.getName());
}
  • Stock
public void decreaseQuantity(int decreaseQuantity) {
    validateDecreaseQuantity(decreaseQuantity);

    if (this.quantity < decreaseQuantity) {
        throw new GlobalException(INSUFFICIENT_STOCK);
    }

    this.quantity -= decreaseQuantity;
}

 

💡동시성 처리 전 테스트 코드 결과

  • 재고 100개에 대해 1개씩 100번 동시 차감했을 때 0이 되어야 하는데, 실제로는 88이 남아 동시성 제어가 제대로 되지 않아 일부 차감이 반영되지 않은 상태에서 발생한 AssertionFailedError가 발생했다.

💡동시성 처리 한 코드

  • Stock
@Getter
@Entity
@Table(name = "p_stock")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Stock extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    @Column(name = "stock_id", nullable = false)
    private UUID stockId;

    @Column(name = "product_id", nullable = false)
    private UUID productId;

    @Column(name = "company_id", nullable = false)
    private UUID companyId;

    @Column(name = "hub_id", nullable = false)
    private UUID hubId;

    @Column(name = "quantity", nullable = false)
    private int quantity;

    @Version
    private Long version;

 

➡️ Stock Entity 에 Version 필드 추가

  • JpaStockRepository
public interface JpaStockRepository extends JpaRepository<Stock, UUID> {

    Optional<Stock> findByProductId(UUID productId);

    @Lock(LockModeType.OPTIMISTIC)
    @Query("SELECT s FROM Stock s WHERE s.productId = :productId")
    Optional<Stock> findByProductWithOptimisticLock(@Param("productId") UUID productId);
}
  • StockRepository
public interface StockRepository {

	Stock save(Stock stock);

	Optional<Stock> findByProductId(UUID productId);
	
	Optional<Stock> findByProductWithOptimisticLock(UUID productId);
}
  • StockRepositoryAdapter
@Repository
@RequiredArgsConstructor
public class StockRepositoryAdapter implements StockRepository {

    private final JpaStockRepository jpaStockRepository;

    @Override
    public Stock save(Stock stock) {
        return jpaStockRepository.save(stock);
    }

    @Override
    public Optional<Stock> findByProductId(UUID productId) {
        return jpaStockRepository.findByProductId(productId);
    }

    @Override
    public Optional<Stock> findByProductWithOptimisticLock(UUID productId) {
        return jpaStockRepository.findByProductWithOptimisticLock(productId);
    }
}
  • applicaiton/service/StockRetryFacade.java 추가
@Component
@RequiredArgsConstructor
public class StockRetryFacade {
    private final StockService stockService; // @Transactional 메서드 가진 서비스

    public StockResult decreaseWithRetry(StockRequests.Decrease request) throws InterruptedException {
        int maxRetries = 5;
        int attempt = 0;

        while (true) {
            try {
                return stockService.decreaseStock(request); // 1회 시도 (새 트랜잭션)
            } catch (ObjectOptimisticLockingFailureException | OptimisticLockingFailureException e) {
                if (++attempt >= maxRetries) throw e;
                Thread.sleep(50);
            }
        }
    }
}

 

➡️ Optimistic Lock은 실패했을 경우 재시도를 해야하기 때문에 facade(퍼사드)를 생성해야한다.

 

💡동시성 처리 후 테스트 코드 결과


참고 자료

1)