락은 동시성 문제를 제어하기 위한 중요한 개념입니다. 오늘의 포스팅에서는 낙관적 락(Optimistic Lock) 과 비관적 락(Pessimistic Lock)에 대해서 알아보도록 하겠습니다.
1. 낙관적 락(Optimistic Lock) 🔒
개념
- 낙관적 락은 데이터 충돌이 드물 것이라고 가정하고 동작합니다.
- 데이터를 읽은 후, 업데이트할 때 변경 충돌이 발생하지 않았는지 확인하는 방식입니다.
- 일반적으로 버전 번호(versioning)를 이용합니다.
작동 방식
- 데이터를 읽을 때 버전 정보를 함께 읽습니다.
- 업데이트 시점에 다시 버전 정보를 확인하여 이전과 동일하면 업데이트를 수행합니다.
- 버전이 변경되었다면, 충돌이 발생했음을 알리고 롤백합니다.
예제코드로 어떻게 구현하고 동작하는지 알아보도록 하겠습니다.
JPA에서의 낙관적 락
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Version;
@Entity
public class Product {
@Id
@GeneratedValue
private Long id;
private String name;
private int stock;
@Version
private int version; // 버전 정보
}
서비스 로직
@Transactional
public void updateStock(Long productId, int quantity) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new IllegalArgumentException("상품이 존재하지 않습니다."));
if (product.getStock() < quantity) {
throw new IllegalStateException("재고가 부족합니다.");
}
product.setStock(product.getStock() - quantity); // JPA가 버전 확인 후 업데이트
productRepository.save(product); // 업데이트 시 버전이 자동으로 증가
}
위의 코드에서 Entity 부분에 `@Version` 어노테이션이 붙은 필드 `version` 이 있습니다. 로직이 실행될 때 조회되는 방식은 다음과 같습니다.
1. `product` 엔티티를 조회하면 @Version 필드 값도 포함되어 조회됩니다.
2. product.setStock(produck.getStock() - quantity) 를 호출한 후, productRepository.save(product) 를 실행하면
3. JPA 는 `UPDATE` 쿼리를 생성할 때 조건에 버전값을 추가합니다.
UPDATE product
SET stock = ?, version = version + 1
WHERE id = ? AND version = ?;
4. 만약 `WHERE` 조건에 있는 `version` 의 값이 일치하지 않으면, JPA는 데이터가 이미 다른 트랙잭션에서 수정되었다고 판단하고 예외(`OptimisticLockExceptino`)를 던집니다.
버전 관리의 중요성✅
- 데이터 충돌의 방지: 여러 사용자가 동시에 데이터를 변경하는 경우, 낙관적 락을 통해 충돌을 가지하고 적절히 처리할 수 있습니다.
- 성능 효율성: 비관적 락처럼 트랜잭션 시작부터 락을 걸어두지 않기때문에, 락 오버헤드가 없습니다.
실제 사용 사례
- 주식 거래 시스템: 주식 매수/매도 정보가 빈번히 조회되지만, 최종적으로 매수/매도를 확정할 때만 쓰기가 일어납니다.
- 쇼핑몰 재고 관리: 조회가 빈번하지만, 재고를 실제로 차감하는 경우는 상대적으로 적습니다.
2. 비관적 락(Pessimistic Lock) 🔒
개념
- 비관적 락은 데이터 충돌이 자주 발생할 것이라고 가정하고 동작합니다.
- 데이터를 읽는 시점에 다른 트랜잭션이 데이터를 수정하지 못하도록 락을 점유합니다.
작동 방식
- 데이터 읽기 시 락을 설정하여 다른 트랜잭션이 데이터에 접근하지 못하도록 합니다.
- 락이 해제되기 전까지 다른 트랜잭션은 대기하거나 예외를 발생시킵니다.
비관적 락 또한 예제 코드로 알아보도록 하겠습니다.
JPA에서의 비관적 락
import jakarta.persistence.LockModeType;
import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class ProductService {
private final EntityManager entityManager;
public ProductService(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Transactional
public void updateStock(Long productId, int quantity) {
Product product = entityManager.find(Product.class, productId, LockModeType.PESSIMISTIC_WRITE);
if (product.getStock() < quantity) {
throw new IllegalStateException("재고가 부족합니다.");
}
product.setStock(product.getStock() - quantity);
entityManager.persist(product); // 변경사항 저장
}
}
위의 코드에서는 entityManager가 `find()` 메서드를 통해 DB에서 `product` 를 조회할 때, LockModeType.PESSIMISTIC_WRITE 옵션을 통해 조회하게 됩니다.
이 옵션은 쓰기 락을 적용하는 방식으로, 충돌 가능성을 애초에 방지하려고 데이터 조회 시점에 바로 락을 걸어 다른 트랙잭션의 접근을 제한합니다. 따라서 호출 즉시 JPA 는 DB에서 `productId`를 조회하면서 즉시 락을 설정합니다. 이 락이 걸린 동안 다른 트랜잭션은 이 데이터에 대한 수정(UPDATE)뿐 아니라 읽기(SELECT) 락이 필요한 작업도 대기 상태가 됩니다.
비관적 락은 트랜잭션을 충돌을 감지하지 않고 아예 예방을 하는 방식입니다. 예를 들어
1. 트랜잭션 A 가 `productId = 1` 에 대해 `PESSIMISTIC_WRITE` 락을 설정하고 작업 중입니다.
2. 트랜잭션 B 가 동일한 `productId = 1` 에 대해 접근하려고 하면, 트랜잭션 A가 완료될 때까지 대기합니다.
실제로 `PESSIMISTIC_WRITE` 옵션을 추가하면 JPA 는 다음과 같은 쿼리를 생성합니다.
SELECT *
FROM product
WHERE id = 1
FOR UPDATE;
이때 `FOR UPDATE` 는 행 수준 락(Row-level Lock) 을 설정하는 SQL 구문입니다. 이 구문은 다른 트랜잭션이 해당 행을 읽거나 수정하지 못하게 차단합니다.
실제 사용 사례
- 은행 시스템: 계좌 간 금액 이체처럼 충돌이 치명적인 경우.
- 좌석 예매 시스템: 다수의 사용자가 동시에 동일한 좌석을 예약하려고 할 때 충돌 가능성이 매우 높습니다.
이렇게 낙관적 락과 비관적 락에 대해서 알아보았습니다. 하지만 이렇게 생각하실 수 있습니다.
애초에 충돌을 예방할 수 있으면 비관적 락을 사용하는게 무조건 효과적이지 않을까?
낙관적 락과 비관적 락은 시스템의 특성에 따라 사용됩니다. 낙관적 락은 장점은 성능과 유연성에 있습니다.
낙관적 락의 사용 이유
낙관적 락은 데이터 충돌이 드물거나, 충돌의 비용이 낮은 환경에서 더 효과적입니다. 아래에서 주요 이유를 살펴보겠습니다:
a. 락 점유 없이 동시성 처리
- 낙관적 락은 데이터를 읽을 때 별도의 락을 걸지 않습니다.
- 이로 인해 비관적 락처럼 락 점유와 해제 과정에서의 추가 오버헤드가 없습니다.
- 예를 들어, 다수의 사용자가 읽기 작업을 수행하는 환경에서 낙관적 락은 비관적 락보다 훨씬 효율적입니다.
b. 데드락 위험이 없음
- 비관적 락은 락 점유 중에 데드락(교착 상태) 위험이 있습니다.
- 낙관적 락은 충돌이 발생하더라도 데이터베이스 수준에서 단순히 실패를 알리기 때문에 데드락 위험이 없습니다.
c. 쓰기 작업이 적은 환경에서 적합
- 낙관적 락은 데이터의 읽기 작업이 빈번하고, 쓰기 작업이 상대적으로 적은 환경에서 매우 효율적입니다.
- 이 환경에서는 대부분 충돌이 발생하지 않으므로, 불필요한 락 설정 비용을 절약할 수 있습니다.
d. 충돌 발생 시 유연한 처리
- 충돌이 발생하면 단순히 재시도하면 됩니다. 일부 비즈니스 로직에서는 이러한 재시도가 큰 부담이 아닙니다.
- 예를 들어, 쇼핑몰에서 재고를 업데이트할 때 낙관적 락을 사용하면 충돌 시 다시 조회-재시도만 하면 되므로 쉽게 처리할 수 있습니다.
이런 내용을 참고하여 자신이 개발하는 프로젝트에서 필요에 따라 낙관적 락과 비관적 락을 효율적으로 채택하여 사용하시면 됩니다. 읽어주셔서 감사합니다.
'Spring' 카테고리의 다른 글
Spring - DB 조회의 최적화 (2) | 2024.11.27 |
---|---|
Spring - Redis 적용 실습 (0) | 2024.11.26 |
Spring - Redis (0) | 2024.11.22 |
Spring - AWS 로 애플리케이션 배포하기 (0) | 2024.11.18 |
Spring - AWS (0) | 2024.11.17 |