Projects/HubEleven

[동시성 처리] Trouble Shooting : Pessimistic Lock 적용 오류

annovation 2026. 2. 19. 16:33

진행 환경

💡진행 환경

  • Java 17
  • Spring Boot 3.5.7
  • MySQL 8.0.44
  • Gradle

문제 상황

💡 문제 상황

  • Pessimistic Lock (낙관적 락) 적용을 위해 @Lock(LockModeType.PESSIMISTIC_WRITE) 추가 후 아래와 같은 오류 발생

 

➡️ TransactionRequiredException: Query requires transaction be in progress

 

💡 문제 증상

  • PESSIMISTIC_WRITE 락 쿼리를 트랜잭션 없이 실행해서 난 오류

원인 분석

💡 원인 1 : 테스트 코드에서 단순히 재고 데이터를 읽어오는 작업이더라도 트랜잭션이 걸려있어야 한다.

  • 현재 테스트 코드에서 레이스 컨디션 검증 포인트는 아래 stockServiceImpl 에서 decreaseStock 을 통해 동시 호출 하는 구간이다.
for (int i = 0; i < threadCount; i++) {
            executorService.submit(() -> {

                try {
                    stockServiceImpl.decreaseStock(
                            StockFixture.decreaseRequest(product, 1)
                    );
                } finally {
                    countDownLatch.countDown();
                }
            });
        }
  • 그런데, JpaStockRepository.findByProductId에 @Lock(PESSIMISTIC_WRITE) 을 통해 동시성 처리를 하고 있기 때문에 findByProductId 와 같은 단순 조회 로직이더라도 SELECT가 아니라 락 쿼리(FOR UPDATE)로 실행되고 이는 트랜잭션 환경이 필수적이다.
public interface JpaStockRepository extends JpaRepository<Stock, UUID> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    Optional<Stock> findByProductId(UUID productId);
}
  • 현재 StockServiceImpl 메서드들은 트랜잭션이 걸려있지만, 테스트 코드에서는 StockServiceImpl 을 거치지 않고 테스트가 Repository를 직접 호출 했기 때문에 해당 호출 자체에는 트랜잭션이 없다.
Stock updatedStock = jpaStockRepository
                .findByProductId(product.getProductId())
                .orElseThrow();

        assertEquals(0, updatedStock.getQuantity());

해결 방안 검토

💡 해결 방안 1 : Lock 없는 메서드로 읽기

 

✅ 새롭게 발견한 문제점 : 단순 조회 API 까지 Lock 조회가 되어 성능이 떨어진다.

@Override
@Transactional(readOnly = true)
public StockResult getStockByProductId(UUID productId) {

    Product product = getProductOrThrow(productId);

    Stock stock = getStockOrThrow(productId);

    return StockResult.from(stock, product.getName());
}
  • 따라서 조회용으로 Lock 이 없는 메서드를 따로 분리하여 설계한다.
public interface JpaStockRepository extends JpaRepository<Stock, UUID> {

    Optional<Stock> findByProductId(UUID productId); // 일반 조회

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("select s from Stock s where s.productId = :productId")
    Optional<Stock> findByProductIdForUpdate(@Param("productId") UUID productId);
}

결과

💡 테스트 코드 정상적으로 통과!