동시성 문제

김인범's avatar
Nov 21, 2024
동시성 문제

트랜잭션과 락(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

taker