레이스 컨디션을 방지하기 위해 어떤 방법을 사용해야 하는지 이어서 글을 쓰려고 한다.
이전글에서 알아보았듯이 @Transactional 을 사용하는 방법이나 synchronized 를 사용하는 방법은
모든 동시성 문제를 해결할 수 없다. 따라서 이번글에서는 레이스 컨디션을 방지하기 위해 데이터베이스에서 사용하는
전략인 페시미스틱 락(Pessimistic Lock) 과 옵티미스틱 락(Optimistic Lock)에 대해 정리해 보려고 한다.
1. 페시미스틱 락(Pessimistic Lock)
페시미스틱 락은 실제 데이터에 Lock을 걸어서 정합성을 맞추는 방법이다. 즉 데이터를 읽고 업데이트 하는 동안 다른 트랜젝션이 해당 데이터에 접근하지 못하도록 락을 걸어 동시성 문제를 방지하는 방법이다. 작동방식은 다음과 같다.
- 트랜젝션이 데이터에 접근하기 전에 락을 획득한다.
- 락을 획득한 트랜잭션만이 데이터를 읽고 변경할 수 있게 락이 걸린다.
- 트랜잭션의 작업이 끝나면 락을 해제하여 다른 트랜잭션이 접근이 가능해 진다.
- 락을 기다리는 동안 다른 트랜잭션은 대기 상태가 된다.
다음 코드는 자바에서 페시미스틱 락을 구현한 예제이다. Spring Boot, Hibernate를 사용한 코드이다.
Entity
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private double balance;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
}
Service
import jakarta.persistence.EntityManager;
import jakarta.persistence.LockModeType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AccountService {
@Autowired
private EntityManager entityManager;
@Transactional
public void withdraw(Long accountId, double amount) {
Account account = entityManager.find(Account.class, accountId, LockModeType.PESSIMISTIC_WRITE);
if (account.getBalance() >= amount) {
account.setBalance(account.getBalance() - amount);
} else {
throw new IllegalStateException("0보다 커야합니다");
}
}
}
위 코드는 다음과 같이 실행된다
- EntityManager.find() 메서드를 사용하여 Account 엔티티를 조회할 때 LockModeType.PESSIMISTIC_WRITE를 사용한다.
- 이로인해 Account에 대한 페시미스틱 락을 획득하고, 다른 트랜잭션이 해당 레코드를 수정하지 못하게 한다.
- 락은 트랜잭션이 완료될 때까지 (메서드 실행이 끝날 때까지) 유지된다.
- PESSIMISTIC_WRITE 유형의 락은 다른 트랜잭션이 해당 데이터를 읽거나 쓰는 것을 막는다.
다음은 페시미스틱 락의 특징이다.
- Lock을 통해 제어하기 때문에 데이터의 정합성이 보장된다. 페시미스틱 락은 데이터를 미리 잠그므로 다른 트랜잭션이 동시에 데이터를 수정할 수 없게 된다. 그러므로 데이터의 일관성과 정합성이 유지된다.
- 별도의 Lock을 잡기 때문에 성능 감소가 있을 수 있다. 그리고 락을 관리하기 위한 추가적인 자원이 필요하며 락을 획득하고 해제하는데 시간이 소요된다. 또한 다른 트랜잭션이 락을 기다리는 동안 대기해야 하므로 전체적인 응답시간이 늘어나게 될 수 있다.
- 다수의 트랜잭션이 서로 다른 순서로 락을 요청하게 되면 데드락이 발생할 수 있다. 이를 방지하기 위해서는 트랜잭션이 리소스를 요청하는 순서를 일관되게 관리해야 하고, 또는 타임아웃을 이용하여 데드락이 발생할 경우 자동으로 중단 및 롤백하도록 하여야 한다.
- 충돌이 자주 일어난다면 이후 알아볼 옵티미스틱 락(Optimistic Lock) 보다 성능이 좋을 수 있다. 옵티미스틱 락은 충돌 감지 후 재시도 과정이 필요하지만 페시미스틱 락은 처음부터 락을 획득함으로써 재시도가 필요 없어 상대적으로 효율적일 수 있다.
2. Optimistic Lock(옵티미스틱 락)
옵티미스틱 락은 실제로 Lock을 이용하지 않고 버전을 이용함으로써 정합성을 맞추는 방식이다.
실제로 데이터를 업데이트하거나 삭제하기 전에 해당 데이터가 마지막 읽힌 이후 변경되었는지를 확인하며 업데이트를 한다. 만약 읽은 버전에서 수정사항이 생겼을 경우에는 다시 읽은 후에 작업을 수행한다. 이 과정을 아래와 같이 나타낼 수 있다.
- 데이터 읽기: 옵티미스틱 락을 사용하는 트랜잭션이 시작될 때, 데이터를 읽고, 이 데이터에 연결된 버전 번호(또는 타임스탬프)도 함께 읽는다. 이 버전 번호는 데이터가 변경될 때마다 증가된다.
- 비즈니스 로직 수행: 애플리케이션은 읽은 데이터를 기반으로 필요한 비즈니스 로직을 수행한다. 이 과정에서 데이터가 변경될 수 있다.
- 변경 사항 적용: 트랜잭션이 데이터를 변경하고 데이터베이스에 쓰기를 시도할 때, 데이터베이스에 저장된 현재 버전 번호와 트랜잭션이 시작할 때 읽었던 버전 번호를 비교한다.
- 버전 확인:
- 4-1 버전 일치: 버전이 일치하면 다른 트랜잭션이 해당 데이터를 수정하지 않았다는 것을 의미하므로, 현재 트랜잭션에 의한 변경이 안전하다는 의미이다. 따라서 데이터베이스는 변경을 수행하고 버전 번호를 증가시킨다.
- 4-2 버전 불일치: 버전이 일치하지 않는 경우, 다른 트랜잭션이 데이터를 변경한 것으로 간주하고 현재 트랜잭션의 변경을 거부한다 (OptimisticLockException 와 같은 예외를 발생시킬 수도 있다).
- 트랜잭션 완료: 트랜잭션이 성공적으로 완료되면 변경 사항이 데이터베이스에 커밋되고, 버전 번호가 업데이트된다. 실패한 경우, 트랜잭션은 롤백되고 데이터는 원래 상태로 복구시킨다.
- 재시도 또는 실패 처리: 버전 불일치로 인해 예외가 발생하면 재시도 로직을 실행하거나 사용자에게 충돌 발생을 알리는 등의 후처리를 할 수 있다.
다음 코드는 자바에서 옵티미스틱 락을 구현한 예제이다.
Entity
import javax.persistence.*;
@Entity
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private double balance;
@Version
private int version; // 버전 필드 추가
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
}
Service
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.OptimisticLockException;
@Service
public class AccountService {
@Autowired
private EntityManager entityManager;
@Transactional
public void withdraw(Long accountId, double amount) throws OptimisticLockException {
Account account = entityManager.find(Account.class, accountId);
if (account.getBalance() >= amount) {
account.setBalance(account.getBalance() - amount);
} else {
throw new IllegalStateException("0보다 커야합니다");
}
entityManager.merge(account);
}
}
위 코드는 다음과 같이 실행된다.
- withdraw 메서드가 실행될 때 엔티티의 버전을 확인한다(@Version version 필드).
- 트랜잭션이 커밋되는 시점에 버전 번호가 변경되었는지 확인한다.
- 다른 트랜잭션에 의해 버전 번호가 이미 변경되었다면 'OptimistickLockException'이 발생한다.
다음은 옵티미스틱 락의 특징이다.
- 옵티미스틱 락은 데이터가 변경될 때까지 충돌을 감지하지 않다가 데이터를 읽을 때 버전 번호나 타임스탬프를 기록하고, 트랜잭션이 커밋되는 시점에 저장된 버전과 현재 버전을 비교하여 변경 여부를 확인한다.
- 옵티미스틱 락 구현에서는 데이터 항목에 버전 번호를 사용한다. 그리고 데이터가 변경될 때마다 이 버전 번호가 자동으로 증가하며, 이를 이용해서 충돌을 감지한다.
- 옵티미스틱 락은 락을 걸지 않기 때문에 락으로 인한 시스템 오버헤드가 없다. 또한 락을 사용하지 않기 때문에 데드락이 발생하지 않는다.
- 충돌이 감지되면 일반적으로 트랜잭션은 일반적으로 실패하고 롤백된다. 이를 처리하기 위해 사용자에게 경고를 주거나 자동으로 재시도 하는 등의 처리를 할 수 있다.
- 충돌이 비교적 드물게 발생하는 환경에서 옵티미스틱 락이 효과적이다. 그러므로 충돌의 빈도가 높은 환경에서는 옵티미스틱 락이 많은 재시도와 성능 저하를 초래한다.
3. 비교
다음은 옵티미스틱 락과 페시미스틱 락을 비교하여 정리한 것이다.
옵티미스틱 락 | 페시미스틱 락 | |
기본 개념 | 데이터 변경 시 충돌을 감지하고 처리 | 데이터 사용 전에 락을 걸어 충돌을 방지 |
작동 방식 | 데이터에 버전 번호를 사용하여 변경 전과 후를 비교 | 데이터베이스 레벨에서 락을 사용하여 데이터를 보호 |
락 오버헤드 | 낮음, 락을 사용하지 않음. | 높음, 락 관리가 필요함. |
데드락 가능성 | 없음. | 있음, 적절한 락 관리 필요. |
충돌 처리 | 충돌 감지 후 롤백 또는 재시도. | 충돌 자체를 방지함. |
성능 | 충돌이 드물 때 높은 성능. | 충돌이 빈번할 때 더 나을 수 있음. |
적합한 환경 | 충돌이 적은, 동시 읽기가 많은 환경. | 충돌 가능성이 높거나 데이터 일관성이 중요한 환경. |
확장성 | 높음, 많은 수의 트랜잭션 처리 가능. | 낮음, 락으로 인해 트랜잭션 처리가 제한될 수 있음. |
일관성 및 안정성 | 데이터 버전 관리를 통해 일관성 유지. | 락을 통해 데이터베이 |
정리하자면
옵티미스틱 락은 데이터베이스에 락을 걸지 않고 버전번호를 통해 데이터 변경 사이 충돌을 감지하고 처리하는 방식이다. 상대적으로 성능이 좋고 확장성이 높지만 충돌이 자주 발생하는 환경인 경우에는 비효율적일 수 있다.
페시미스틱 락의 경우 DB에 락을 걸어 데이터를 보호하고 이를 통해서 데이터의 일관성 및 안정성을 높힌다. 그러나 락으로 인한 오버헤드가 있으며, 데드락을 관리해야 한다.
그렇다면 옵티미스틱 락과 페시미스틱 락 두 기법으로 모든 동시성 문제를 해결할 수 있을까?
그렇지 않다. 그 이유와 다른 해결책들에 대해 다음글에서 계속하겠다.
'서버 개발' 카테고리의 다른 글
Spring Boot REST API 회원관리 - (1) 환경구성 및 "Hello" 출력 (0) | 2024.07.30 |
---|---|
레이스 컨디션 (Race Condition) -03. Lettuce, Redisson (0) | 2024.04.24 |
CURL로 외부 서버와 통신 테스트(+ 겪을 수 있는 에러로그) (0) | 2024.03.25 |
내부망에서 외부 서버와의 통신시 발생할 수 있는 문제들 (0) | 2024.03.20 |
확장 가능한 시스템 설계 예시 (0) | 2023.10.22 |