Contents
예시 코드트랜잭션과 락(lock) 메커니즘을 사용해 동시성 문제 관리하기.
데이터베이스 관리 시스템(DBMS)에서는 동시성 문제가 발생하는 상황을 방지하기 위해
ACID(Atomicity, Consistency, Isolation, Durability) 를 보장하는 트랜잭션을 제공한다고 합니다.
두 가지 방식으로 문제를 해결하는 코드가 있습니다.
ㅤ | 비관적 락 | 낙관적 락 |
방식 | 데이터를 읽는 순간부터
다른 트랜잭션이 해당 데이터를 변경하지 못하도록
잠금을 건다. | 데이터를 수정하기 전에
다른 트랜잭션에 의해 데이터가 변경되지 않았음을
검증하는 방식 |
차이점 | 데이터베이스에 락을 걸기에,
성능이 저하될 수 있음 | 성능 오버헤드가 적다.
충돌 발생 시 재시도가 필요하다. |
선택 기준 | 동시성이 높은 환경에서
데이터 충돌이 자주 발생하는 경우 적합. | 충돌이 적은 환경에서 적합 |
예시 코드
비관적 락 (Pessimistic Lock)
비관적 락은 데이터를 읽는 순간부터 다른 트랜잭션이 해당 데이터를 변경하지 못하도록
잠금을 거는 방식입니다.
- ENTITY 클래스
@Entity
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private int balance;
// Getter와 Setter
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
}
- Repository 클래스
public interface AccountRepository extends JpaRepository<Account, Long> {
// 비관적 락 적용: SELECT FOR UPDATE
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT a FROM Account a WHERE a.id = :id")
Account findByIdWithLock(Long id);
}
- Service 클래스
@Service
public class AccountService {
private final AccountRepository accountRepository;
public AccountService(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
@Transactional
public void withdraw(Long accountId, int amount) {
Account account = accountRepository.findByIdWithLock(accountId);
if (account.getBalance() < amount) {
throw new IllegalArgumentException("잔액이 부족합니다.");
}
account.setBalance(account.getBalance() - amount);
}
}
낙관적 락 (Optimistic Lock)
낙관적 락은 데이터를 수정하기 전에
다른 트랜잭션에 의해 데이터가 변경되지 않았음을 검증하는 방식입니다.
- ENTITY 클래스
@Entity
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private int balance;
@Version // 낙관적 락을 위한 버전 필드 ★중요
private int version;
// Getter와 Setter
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
}
- Repository 클래스
public interface AccountRepository extends JpaRepository<Account, Long> {}
- Service 클래스
@Service
public class AccountService {
private final AccountRepository accountRepository;
public AccountService(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
@Transactional
public void withdraw(Long accountId, int amount) {
Account account = accountRepository.findById(accountId)
.orElseThrow(() -> new IllegalArgumentException("계좌가 존재하지 않습니다."));
if (account.getBalance() < amount) {
throw new IllegalArgumentException("잔액이 부족합니다.");
}
account.setBalance(account.getBalance() - amount);
accountRepository.save(account); // @Version을 통해 동시성 충돌 검출
}
}
Share article