여러 스레드가 동시에 하나의 데이터에 접근해서 값을 수정하는 경우 race condition(경쟁 조건)이 발생할 수 있다.
이를 해결하기 위한 방법에는 여러가지가 있는데 이번 포스팅에는 Optimistic Lock을 활용하여 해결해 보도록 한다.
데이터베이스 동시성 이슈 해결하기 시리즈
2. Pessimistic Lock
3. Named Lock
4. Redis Lettuce
5. Redis Redisson
Optimistic Lock(낙관적 락)
낙관적 락은 여러 사용자가 동시에 같은 데이터를 수정하는 경우가 드물다고 가정할 때 주로 사용한다.
따라서 Lock을 걸지 않고 충돌이 발생하면 그때마다 엔티티의 버전을 비교하여 동시성 이슈를 해결하는 방식이다.
즉, DB 트랜잭션을 이용하는 것이 아니라 애플리케이션 레벨에서 지원하는 Lock이다.

Optimistic Lock 적용하기
Spring Data JPA에서는 @Version 어노테이션을 통해 엔티티의 버전을 관리함으로써 Optimistic Lock을 적용할 수 있다.
@Version 어노테이션 적용이 가능한 자료형은 Timestamp, Long(long), Integer(int), Short(short)이다.
아래의 Stock 엔티티에서는 version 필드에 @Version 어노테이션을 적용했다.
@Entity
public class Stock {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long productId;
private Long quantity;
@Version
private Long version;
}
@Service
@RequiredArgsConstructor
public class OptimisticLockService {
private final StockRepository stockRepository;
@Transactional
public void decrease(Long id, Long quantity) {
Stock stock = stockRepository.findByIdWithOptimisticLock(id);
stock.decrease(quantity);
stockRepository.saveAndFlush(stock);
}
}
Optimistic Lock을 적용하려면 @Version 어노테이션만 붙여서는 안된다.
StockRepository에 선언된 추상메서드에 @Lock 어노테이션을 적용해야 한다.
public interface StockRepository extends JpaRepository<Stock, Long> {
@Lock(value = LockModeType.OPTIMISTIC)
@Query("select s from Stock s where s.id = :id")
Stock findByIdWithOptimisticLock(@Param("id") Long id);
}
@Lock 어노테이션을 선언할 때 LockModeType을 설정할 수 있는데 사용할 수 있는 타입은 아래와 같다.
- LockModeType.NONE
- 별도로 락 옵션을 지정하지 않아도 엔티티에 @Version 어노테이션을 적용하면 기본으로 적용되는 옵션이다.
- 조회한 엔티티를 수정하는 시점에 다른 트랜잭션으로부터 해당 데이터가 변경되지 않았음을 보장하며, 만약 데이터가 변경되었다면 예외가 발생한다. 조회 시점부터 수정 시점까지 동일한 데이터를 보장한다는 의미이다.
- LockModeType.OPTIMISTIC
- LockModeType.NONE의 경우 엔티티를 수정해야 버전을 체크하지만, OPTIMISTIC 옵션은 엔티티를 조회만 해도 버전을 체크한다. 이 옵션은 트랜잭션을 커밋하는 시점에 버전 정보를 체크하기 때문에 엔티티의 조회 시점부터 트랜잭션이 커밋될 때 까지 동일한 데이터를 보장한다는 의미이다.
- LockModeType.OPTIMISTIC_FORCE_INCREMENT
- 엔티티가 물리적으로 변경되지는 않았지만, 논리적으로 변경되었을 경우 버전을 증가시키고 싶을 때 사용한다. 가령 게시물과 첨부파일 엔티티가 1:N 관계일 때 게시물에 첨부파일이 하나 추가된 상황은 게시물 엔티티의 물리적 변경은 일어나지 않았지만, 논리적인 변경은 일어났다. 이때 버전을 변경하고 싶다면 해당 락 옵션을 사용하면 된다.
- 엔티티가 직접 수정되어 있지 않아도, 트랜잭션을 커밋할 때 UPDATE 쿼리를 사용해 버전 정보를 강제로 증가시킨다.
이제 Optimistic Lock 설정을 모두 완료했다.
이 상태에서 만약 Stock 엔티티의 quantity 필드가 수정된다면, 트랜잭션을 커밋하고 영속성 컨텍스트를 flush 하는 시점에 아래와 같은 Update 쿼리가 실행된다.
UPDATE STOCK
SET
quantity = ?,
version = ? # version + 1
WHERE
id = ?,
and version = ? # 버전 비교
STOCK 테이블을 업데이트 할 때, WHERE 절에서 조회했을 때의 version을 비교하여 일치하는 확인한다.
만약 version이 일치하면 데이터 조회 이후 quantity 컬럼이 수정되지 않았다는 의미이고, version이 불일치 하여 업데이트를 하지 못했다면 데이터 조회 이후 quantity 컬럼이 수정되었다는 의미이다.
후자의 경우 예외가 발생하기 때문에 @Transaction 어노테이션을 사용하여 롤백 후 재시도 처리 로직을 작성해야 한다.
Optimistic Lock 적용 시 주의할 점
@Version 어노테이션이 달린 필드는 JPA가 직접 관리하는 필드이므로 임의로 수정하면 안된다.
하지만 Version 필드를 개발자가 수정해야 하는 경우가 있는데 벌크 연산을 하는 경우이다. 벌크 연산의 경우 JPA가 버전을 무시하므로 버전 필드를 강제로 증가시키는 작업을 해야 한다.
update Stock s set s.quantity = '변경할 값', s.version = s.version + 1