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