Optimistic Lock , 시도와 실패

이광훈·2024년 9월 16일

낙관적 Lock 을 활용하는 방법이다. 이 방법은 데이터를 조회할 때 가져온 해당 데이터의 version 과 수정할 때 확인한 데이터의 version 을 비교하여 version 이 다를 경우, 조회와 수정 로직을 어플리케이션 레벨에서 다시 수행하는 것이다.

public class Stock {  
  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    private Long id;  
  
    private Long productId;  
  
    private Long quantity;  
  
    @Version  ## @Version 어노테이션 추가
    private Long version;
}

@Lock(LockModeType.OPTIMISTIC)  
@Query("select s from Stock s where s.productId = :productId")  
public Stock findStockByProductIdWithOptimisticLock(Long productId);
  • Entity 에 Version 어노테이션을 추가하고, 해당 JpaRepository 에 @Lock 어노테이션을 이용해 optimistic lock 을 사용할 수 있다.
@Service  
public class OptimisticStockService {  
  
    private final StockRepository stockRepository;  
  
    public OptimisticStockService(StockRepository stockRepository) {  
        this.stockRepository = stockRepository;  
    }  
  
    @Transactional  
    public void decrease(Long productId , Long quantity){  
  
        Stock stock = this.stockRepository.findStockByProductIdWithOptimisticLock(productId);  
        log.info("Current quantity = {}" , stock.getQuantity());  
        stock.decrease(quantity);  
        this.stockRepository.save(stock);  
    }  
}

public class OptimisticLockStockFacade {  
  
    private final OptimisticStockService optimisticStockService;  
  
    public OptimisticLockStockFacade(OptimisticStockService stockService) {  
        this.optimisticStockService = stockService;  
    }  
  
    public void decrease(Long id , Long quantity) throws InterruptedException {  
  
        while (true){  
            try{  
                this.optimisticStockService.decrease(id , quantity);  
                break;  
            }catch (Exception e){  
                Thread.sleep(50);  
            }  
        }  
    }  
}

  • 그리고 OptimisticStockService 와 이를 호출하는 OptimisticStockServiceFacade 를 만들어 Facade 에서 OptimisticStockService 를 호출하는 방식으로 이를 이용할 수 있다.

  • 여기서 의문이 생긴 지점이 있었다. 왜 Facade 를 이용할까? 그냥 Service 에서 직접 While 을 사용하여 lock 의 version 이 맞을 때 까지 재시도 할 수 있지 않을까?

  • 그래서 직접 코드를 작성해보았다.

@Transactional  
public void decreaseStockWithOptimisticLock(Long productId , Long quantity){  
  
    Stock stock;  
  
    while(true){  
  
        try{  
            stock = this.stockRepository.findStockByProductIdWithOptimisticLock(productId);  
            log.info("Current quantity = {}" , stock.getQuantity());  
            stock.decrease(quantity);  
            this.stockRepository.save(stock);  
            break;  
        }catch (Exception e){  
            System.out.println("e = " + e);  
            log.info("Retrying OptimisticLock Decreasing");  
        }  
  
    }  
}
  • 하지만 결론적으로 어림도 없지, 바로 테스트에서 실패했다.

  • 예상은 0개를 했지만 85개가 남음, 결론적으로 optimistic Lock 이 제대로 동작하지 않은것이다. ==왜 일까?== 생각해보니 이 메서드에는 문제점이 있다.
  1. OptimisticLockException 이 발생하는 시점은 entity 가 flush 되는 시점이다. 지금 위의 함수에서는 this.stockRepository.save() 를 이용해 entity 를 영속성 컨텍스트에 반영하지만, 이를 flush 하는것은 아니다. 따라서 이 트랜잭션 작업의 flush 는 commit 되는 시점, 즉 현재 stockService 가 아니라 proxy 객체에서 일어난다. 따라서 현재 메서드에서 Exception 으로 OptimisticLockException 을 잡을 수 없다.

그러면 save 말고 saveAndFlush 를 사용하면 어떨까? 이 메서드는 entity 를 영속성 컨텍스트에 저장하고 이를 db 에 flush 작업까지 수행해준다.

@Transactional  
public void decreaseStockWithOptimisticLock(Long productId , Long quantity){  
  
    Stock stock;  
  
    while(true){  
  
        try{  
            stock = this.stockRepository.findStockByProductIdWithOptimisticLock(productId);  
            log.info("Current quantity = {}" , stock.getQuantity());  
            stock.decrease(quantity);  
            this.stockRepository.saveAndFlush(stock);  
            break;  
        }catch (Exception e){  
            System.out.println("e = " + e);  
            log.info("Retrying OptimisticLock Decreasing");  
        }  
  
    }  
}

  • 이것도 실패다,,, 무한 루프가 돈다. 일단 ObjectOptimisticLockingFailureException 이 발생한다. 하지만, 여기에도 문제점이 있다. 해당 exception 이 발생하면, 다시 try 블럭내의 코드를 실행한다. 그런데 여기서 stockRepository 에서 가져오는 값이 DB 에 있던 값이 아닌 현재 TX 내의 영속성 컨텍스트 내의 값이라는 것이다. 그러면, 그냥 계속 동일한 엔티티를 가져오는 결과다.

마지막으로 그러면 @Transactional 어노테이션을 없애면 어떨까? 그러면 영속성 컨텍스트의 값이 아닌 DB 의 값에 직접 접근하니까 가능하지 않을까?

  • 이것마저 실패다,,, saveAndFlush 메서드를 사용하려면 TX 가 필요한 듯 하다. save() 메서드와 flush() 메서드가 한번에 묶여서 실행해야 의미가 있어서 그러는 것 아닐까 추측한다,,,,,

  • 결론적으로 강의에서 나온 방법대로, service 와 이 service 의 메서드를 호출하는 facade를 이용해야 할 것 같다. 적용은 실패했지만,,, 그래도 여러가지 생각을 많이 해보고 적용까지 해보는 시간이었고 얻어간 점도 많은 것 같다.
profile
허허,,,

0개의 댓글