우당탕탕

[동시성 제어 3편] 낙관적 락(Optimistic Lock) - @Version 어노테이션을 활용한 락 본문

Tech/Spring

[동시성 제어 3편] 낙관적 락(Optimistic Lock) - @Version 어노테이션을 활용한 락

모찌모찝 2025. 8. 6. 08:55
[동시성 제어 3편] 낙관적 락(Optimistic Lock)


안녕하세요!
지난 글에서는 비관적 락(Pessimistic Lock)에 대해 알아보았습니다. 오늘은 그 반대 성격의 낙관적 락(Optimistic Lock)을 실전 코드 중심으로 정리해 보려고 합니다.

이전 편 보러 가기

[동시성] 동시성 제어란? - 1편 (데이터가 꼬이지 않는 백엔드의 첫걸음)

[동시성 2편] 비관적 락(Pessimistic Lock) - JPA 스프링으로 경험해 보는 실전 가이드

동시성문제 : 출처:https://blog.naver.com/chgy2131/223374232041

1. 낙관적 락(Optimistic Lock)이란?

  • 이름 그대로 "충돌이 자주 일어나지 않을 것"이라는 낙관적 가정!
  • 여러 트랜잭션이 동시에 데이터를 읽고, 수정 시점에만 충돌이 있는지 검사
  • ▶️ 행(row)을 잠그지 않고, 데이터를 자유롭게 읽게 한 뒤, 커밋 전 버전 필드(Version)나 타임스탬프를 비교해 바뀌었으면 롤백 → 재시도 진행

언제 사용할까?!

  • 읽기(Read) 작업이 많고, 실제 ‘같은 데이터에 동시에 쓰기’ 극히 드문 환경
  • 비관적 락 대비 경합/성능 저하가 거의 없어 대규모 서비스에서 많이 씀

-> 경합( 동시에 같은 데이터 쓰는 빈도 ) 발생이 낮은 경우에 사용하기 좋음 

2. 낙관적 락 동작원리 

1. 여러 트랜잭션이 동시에 동일 데이터를 조회
2. 각각 로컬에서 데이터 수정
3. 커밋 시점에, 내가 읽은 Version이 같은지를 비교
4. DB의 Version(혹은 Timestamp)와 내가 읽은 Version이 같은지를 비교
5. 버전이 같다면 저장(업데이트), 다르다면 예외(충돌) -> 롤백 & 재시도 진행 

3. 실전 코드 

1. Version 어노테이션 활용

import javax.persistence.*;

@Entity
public class Stock {
    @Id @GeneratedValue
    private Long id;

    private Integer quantity;

    @Version
    private Long version; // 낙관적 락의 핵심!
    // getter, setter 등
}

2. 서비스 코드 예시

@Service
public class StockService {
    @Autowired
    private StockRepository stockRepository;

    @Transactional
    public void decrease(Long stockId, int amount) {
        Stock stock = stockRepository.findById(stockId).orElseThrow();
        if (stock.getQuantity() < amount) throw new RuntimeException("재고 부족");

        stock.setQuantity(stock.getQuantity() - amount);
        // 트랜잭션 커밋 시, JPA가 version필드 자동 체크
        // 충돌 발생 시 OptimisticLockException 예외 발생
    }
}
  • 여러 사용자가 동시에 같은 Stock을 수정해 커밋하면?
    • 먼저 저장한 쪽은 성공
    • 이후 저장 시 @Version 값이 달라져 OptimisticLockException 발생 → 롤백

3. 실패 시 재시도(Retry) 구현 예시

public void decreaseWithRetry(Long stockId, int amount) {
    boolean success = false;
    while (!success) {
        try {
            decrease(stockId, amount);
            success = true;
        } catch (ObjectOptimisticLockingFailureException e) {
            // 충돌 난 경우 재조회 & 재시도 (재고 남았는지 또 확인!)
        }
    }
}

낙관적 락 충돌 시에는 트랜잭션을 다시 수행(재시도)하는 패턴 활용

4. 실무 팁

  • 낙관적 락은 “충돌이 드문 곳”에서만 효율적
    • 쓰기/수정이 매우 잦은 경우엔 성능 저하 (충돌 → 재시도 루프 반복)
  • JPA @Version 필드는 꼭 엔티티에 포함
  • 충돌 발생 시, 적절한 예외처리/사용자 알림/로그 기록
  • 꿀팁: “수량 부족” 등 단순한 비즈니스 검증도 같이 체크 필요

5. 낙관적 락이 필요한 예시

  • 쇼핑몰 주문, 재고 감소(동시 주문 안심 환경)
  • 게시글 수정(동시 편집 감지용)
  • 마이크로서비스/분산 시스템에서 동기화 필요할 때

6. 장/단점 

장점 단점
락 경합이 적고 성능 향상 동시 쓰기 충돌시 실패 후 재시도 필요
읽기 작업 많을 때 효율적 충돌 많이 나는 환경에선 비관적 락이 유리
구현 쉽고 JPA에서 지원 실시간성 요구 시 충돌-재시도 반복에 주의


낙관적 락
은 조회/읽기는 자유롭게, “마지막 쓰기 시점에만” 충돌 검증을 하게 됩니다.
JPA의 @Version으로 정말 간단하게 구현가능 하지만 “충돌 빈도”에 따라 언제 쓸지 현명한 선택이 중요합니다. 
다음 편에서는 실전에서 많이 사용하고 성능까지 좋은 분산 락(Redis/Redisson)에 대해 상세하게 작성해 볼 예정입니다.

궁금한 예시, 실전 에러/해결법, @Version 이외 락 방식 등 질문은 언제든 댓글로 남겨주세요!

Comments