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)
'Projects > HubEleven' 카테고리의 다른 글
| [동시성 처리] Trouble Shooting : Pessimistic Lock 적용 오류 (0) | 2026.02.19 |
|---|---|
| [동시성 처리] DB Lock : Pessimistic Lock (비관적 락) - 실습 (0) | 2026.02.18 |
| [동시성 처리] Trouble Shooting : 멀티스레드 재고 감소 통합 테스트 코드 Eureka Server Connection refused (0) | 2026.02.16 |
| [동시성 처리] synchronized vs Reentrant Lock - 실습 (0) | 2026.02.15 |
| [동시성 처리] JVM Lock : synchronized VS Reentrant Lock (실습중..) (0) | 2026.02.14 |