어플리케이션을 개발하다 보면 낙관적 락, 비관적 락에 대해 들어본 적 있을 것이다
동시성 제어를 다루다 보면 한 번쯤은 마주치는 개념이다
락은 언제 필요한가?
여러 사용자가 동시에 같은 데이터를 수정하려고 할 때 문제가 생길 수 있다
- 재고 마이너스: 실제보다 많은 주문이 들어가서 재고가 음수로 떨어짐
- 중복 결제: 같은 주문에 대해 여러 번 결제 처리됨
- 데이터 덮어쓰기: 두 사람이 동시에 수정해서 한쪽 수정 내용이 사라짐
[상황]
- 상품 재고: 10개
- 사용자 A: 5개 주문
- 사용자 B: 7개 주문 (거의 동시에)
[문제]
1. A가 재고 확인: 10개 → OK
2. B가 재고 확인: 10개 → OK
3. A가 재고 차감: 10 - 5 = 5개 저장
4. B가 재고 차감: 10 - 7 = 3개 저장 (A의 차감 무시됨)
결과: 실제로는 12개가 팔렸지만 재고는 3개
이 처럼 동시성 문제가 발생할 수 있고, 락은 이런 상황을 방지하기 위해 사용한다
비관적 락 (Pessimistic Lock)
개념
"충돌이 일어날 거야. 미리 막자"
데이터를 읽는 순간 락을 건다
트랜잭션이 끝날 때까지 다른 사람은 변경을 할 수 없다
동작 방식
-- 1. 락을 걸면서 조회 (SELECT ... FOR UPDATE)
SELECT * FROM product WHERE id = 1 FOR UPDATE;
-- 이 시점에 다른 트랜잭션에서:
-- 일반 조회 (SELECT)? → 가능! 락 안 걸린 조회는 문제없음
-- 락을 건 조회 (SELECT ... FOR UPDATE)? → 대기
-- 수정 (UPDATE)? → 대기
-- 삭제 (DELETE)? → 대기
-- 2. 안전하게 재고 차감
UPDATE product SET stock = stock - 1 WHERE id = 1;
-- 3. 커밋하면 락 자동 해제
COMMIT;
이 키워드가 붙으면 해당 행에 배타적 락(Exclusive Lock)이 걸린다
FOR UPDATE로 비관락을 사용하게 되면 다른 트랜잭션의 동일 로직이 수행될 때 락이 해제될 때 까지 대기하게 된다
// 비관적 쓰기 락
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM Product p WHERE p.id = :id")
Optional<Product> findByIdWithPessimisticLock(@Param("id") Long id);
JPA에서는 Lock 어노테이션에 Type을 PESSIMISTIC_WRITE로 지정하면 비관적 락을 적용할 수 있다
비관적 락이 안전한가?
비관적 락에 대한 내용을 보면 FOR UPDATE가 모든 것을 막아줄 것 같고, 해결해줄 것 같아보인다
FOR UPDATE는 조회 제한하지 않는다
조회를 제한하지 않기 때문에 FOR UPDATE로 락을 건 이후에 다른 로직에서 해당 데이터를 조회하여 업데이트를 시도한다면 락이 해제되는 순간 데이터가 변경되게 되어 Lost Update 문제가 발생하게 된다
// 로직 A와 B는 별개의 비즈니스 로직이라고 가정
// 트랜잭션 A
// 로직 A
@Transactional
fun processOrder(productId: Long, quantity: Int) {
val product = productRepository.findByIdForUpdate(productId)
log.info("트랜잭션 A - 조회한 재고: {}", product.stock) // 100
product.decreaseStock(quantity) // 100 - 50 = 50
productRepository.save(product)
log.info("트랜잭션 A - 저장할 재고: {}", product.stock) // 50
// 커밋 시점에 재고 = 50으로 저장
}
// 트랜잭션 B
// 로직 B
@Transactional
fun updatePrice(productId: Long, quantity: Int) {
val product = productRepository.findById(productId).orElseThrow() // 일반 SELECT!
log.info("트랜잭션 B - 조회한 재고: {}", product.stock) // 100 (락 없이 조회)
product.decreaseStock(quantity) // 100 - 30 = 70
productRepository.save(product)
log.info("트랜잭션 B - 저장할 재고: {}", product.stock) // 70
// ⚠️ 커밋 시점에 재고 = 70으로 덮어씀!
}
위와 같은 Lost Update 문제가 발생하여 정합성에 문제가 생길 수 있다
비관적 락을 사용한다면 위와 같은 문제를 고민해보고 사용해야 한다
낙관적 락 (Optimistic Lock)
개념
"충돌은 잘 안 일어나. 일단 믿고 진행하자"
데이터를 읽을 때는 락을 걸지 않는다
수정할 때 버전을 확인해서 변경되었으면 실패 처리한다
동작 방식
-- 1. 버전과 함께 조회 (락 없음)
SELECT id, stock, version FROM product WHERE id = 1;
-- 결과: id=1, stock=100, version=1
-- 2. 업데이트 시 버전 확인
UPDATE product
SET stock = 99, version = 2
WHERE id = 1 AND version = 1;
-- 만약 다른 트랜잭션이 처리되며 version을 2로 바꿨다면?
-- → 영향받은 행 수 = 0 (업데이트 실패)
-- → OptimisticLockException 발생
버전 필드를 추가하여 데이터 변경 시마다 버전을 증가시킨다
업데이트 시 조회했던 버전과 현재 버전이 같은지 확인하고, 다르면 누군가 먼저 수정한 것이므로 실패 처리한다
@Entity
class Product(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
var stock: Int,
@Version
var version: Long = 0
) {
fun decreaseStock(quantity: Int) {
require(stock >= quantity) { "재고 부족" }
stock -= quantity
}
}
interface ProductRepository : JpaRepository<Product, Long>
// Service
@Transactional
fun processOrder(productId: Long, quantity: Int) {
val product = productRepository.findById(productId).orElseThrow()
product.decreaseStock(quantity)
productRepository.save(product)
// version이 변경되었다면 OptimisticLockException 발생
}
JPA에서는 엔티티에 @Version 어노테이션만 추가하면 낙관적 락이 자동으로 적용된다
낙관적 락의 재시도 처리
낙관적 락은 충돌이 발생하면 예외가 발생하므로, 고객 경험이나 비즈니스적으로 필요한 경우 재시도 로직이 필요하다
비관적 락 vs 낙관적 락
비관적 락은 언제 쓸까?
- 충돌이 자주 발생하는 경우: 동시 수정이 잦은 데이터
- 충돌 비용이 큰 경우: 재시도하기 어려운 중요한 작업 (결제, 송금)
- 트랜잭션이 짧은 경우: 락을 오래 잡고 있지 않아도 되는 경우
장점: 데이터 정합성 보장이 확실함
단점: 락 대기로 인한 성능 저하, 데드락 위험
낙관적 락은 언제 쓸까?
- 충돌이 드문 경우: 동시 수정 확률이 낮은 데이터
- 읽기가 많은 경우: 조회는 많지만 수정은 적은 경우
- 응답 속도가 중요한 경우: 락 대기 없이 빠른 응답이 필요한 경우
장점: 락을 걸지 않아 성능이 좋음, 데드락 없음
단점: 충돌 시 재시도 필요, 재시도 로직 구현 필요
마무리
동시성 제어에 정답은 없다
비관적 락은 확실하지만 자원을 계속 점유한다
낙관적 락은 빠르지만 재시도가 필요하다
각자 trade-off가 있을 뿐이다
그렇다면 어떻게 선택할까?
- 충돌이 얼마나 자주 일어나는가
- 충돌이 일어났을 때 비용이 얼마나 큰가
처음부터 완벽하게 선택할 필요는 없다
낙관적 락으로 시작해서 충돌률을 보고, 필요하면 비관적 락으로 바꾸거나 분산락이나 다른 락들을 고민해보면 된다
'공부' 카테고리의 다른 글
| 서킷브레이커를 알았더라면 (0) | 2026.03.20 |
|---|---|
| 인덱스 만능이 아니다 (0) | 2026.03.09 |
| 어플리케이션 서비스와 도메인 서비스 (0) | 2026.02.27 |
| 코드 부터 치지 말자 - 설계 문서 작성 (0) | 2026.02.13 |
| WIL 회고 - TDD 진짜 합니다 (26.02 WEEK 1) (0) | 2026.02.08 |