[스탠다드] 동시성 제어

토리·2025년 2월 24일
0

락 이란

  • 락(lock)은 컴퓨팅에서 주로 데이터베이스 관리 시스템(DBMS)이나 다중 스레딩 환경에서 사용하는 중요한 개념
  • 락은 특정 자원에 대한 접근을 제어하여 동시성을 관리하고 데이터의 일관성 및 무결성을 유지하는 메커니즘
  • 자원 공유: 여러 사용자나 프로세스가 동일한 데이터에 동시에 접근하려 할 때, 락은 이들 중 하나만이 데이터를 수정할 수 있도록 허용하여 데이터의 일관성을 보장
  • 데이터 무결성: 데이터베이스에서 트랜잭션이 실행되는 동안 데이터 무결성을 유지하기 위해 락이 사용됨
  • 예를 들어, 은행 계좌에서 금액을 이체하는 경우, 락은 이체 과정에서 계좌의 잔액이 정확하게 유지되도록 함

락의 종류

  • 공유 락(Shared Lock):
    • 공유 락은 데이터를 읽을 때 사용
    • 공유 락이 걸린 데이터는 다른 사용자도 읽을 수 있지만, 수정은 할 수 없음
    • 이를 통해 많은 사용자가 동시에 데이터를 안전하게 읽을 수 있음
  • 독점 락(Exclusive Lock):
    - 독점 락은 데이터를 수정할 때 사용됨
    - 독점 락이 걸린 데이터는 해당 락을 소유한 사용자만이 데이터를 읽거나 수정할 수 있음
    - 다른 사용자는 그 데이터에 접근할 수 없음

1. 비관적 동시성 제어 (Pessimistic Concurrency Control)

  • 비관적 동시성 제어는 충돌이 발생할 것이라고 "비관적"으로 가정하고, 데이터에 접근하기 전에 락을 사용하여 해당 데이터를 보호함.
  • 주로 데이터베이스 트랜잭션이 길거나 충돌 가능성이 높을 때 사용됨
  • 데이터를 읽거나 수정하려는 동안 해당 데이터를 잠그고 (락을 걸고), 다른 트랜잭션이 해당 데이터에 접근하는 것을 방지
  • 장점: 데이터 무결성을 확실히 보장
  • 단점: 락으로 인해 시스템의 처리 성능이 저하될 수 있으며, 데드락 발생 가능성이 있음
import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import javax.persistence.PersistenceContext;
import javax.transaction.Transactional;

public class AccountService {

    @PersistenceContext
    private EntityManager entityManager;

    @Transactional
    public void withdraw(Long accountId, double amount) {
        // Account 엔티티를 찾고 비관적 락을 건다
        Account account = entityManager.find(Account.class, accountId, LockModeType.PESSIMISTIC_WRITE);
        if (account.getBalance() >= amount) {
            account.setBalance(account.getBalance() - amount);
        } else {
            throw new RuntimeException("Insufficient funds");
        }
        // 변경사항을 데이터베이스에 반영
        entityManager.merge(account);
    }
}
  • 데이터베이스에서 Account 엔티티 검색: EntityManager를 사용하여 특정 accountId에 해당하는 Account 엔티티를 검색하고, PESSIMISTIC_WRITE 락을 걸어 다른 트랜잭션이 동시에 같은 데이터를 수정하지 못하게 함
  • 잔액 검사 및 수정: 검색된 계좌의 잔액이 인출하려는 금액보다 크거나 같은지 확인함. 충분한 경우, 잔액에서 해당 금액을 차감. 잔액이 부족한 경우 RuntimeException을 발생시켜 트랜잭션을 롤백함
  • 변경사항 반영: 변경된 Account 엔티티를 entityManager.merge(account)를 통해 데이터베이스에 반영. 이 메소드는 트랜잭션이 성공적으로 완료되면 자동으로 변경 사항을 커밋하고, 실패할 경우 롤백

2. 낙관적 동시성 제어 (Optimistic Concurrency Control)

  • 낙관적 동시성 제어는 충돌이 자주 발생하지 않는다고 "낙관적"으로 가정하고, 트랜잭션이 데이터를 커밋할 때만 충돌을 검사.
  • 주로 읽기 작업이 많고, 쓰기 작업이 상대적으로 적을 때 적합
  • 데이터에 대한 락 없이 트랜잭션을 수행하고, 커밋 시점에 변경 사항이 있는지 확인하여 충돌을 검사
  • 장점: 동시성 수준이 높고 시스템의 처리 성능에 미치는 영향이 비교적 적음
  • 단점: 충돌 발견 시 이미 수행한 모든 작업을 취소(롤백)해야 할 수 있음
import javax.persistence.*;

@Entity
@Table(name = "accounts")
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    private double balance;

    @Version
    private int version; //이거 낙관적락

    // Getters and setters
}

@Transactional
public void withdraw(Long accountId, double amount) {
    EntityManager em = entityManagerFactory.createEntityManager();
    EntityTransaction transaction = em.getTransaction();
    transaction.begin();

    try {
        Account account = em.find(Account.class, accountId);
        if (account.getBalance() >= amount) {
            account.setBalance(account.getBalance() - amount);
            em.merge(account);
            transaction.commit();  // 변경사항 커밋, 여기서 버전 충돌 검사 발생
        } else {
            throw new RuntimeException("Insufficient funds");
        }
    } catch (OptimisticLockException ole) {
        transaction.rollback();
        System.out.println("Transaction conflict detected: " + ole.getMessage());
    } finally {
        em.close();
    }
}
  • 데이터베이스에서 Account 엔티티를 조회
  • 계좌의 잔액을 갱신
  • 트랜잭션을 커밋할 때, JPA는 자동으로 엔티티의 버전 번호를 검사
  • 만약 다른 트랜잭션이 이미 해당 엔티티를 수정하여 버전 번호가 변경된 경우, OptimisticLockException이 발생
  • 예외가 발생하면 롤백을 수행하고, 예외 없이 성공적으로 커밋되면 데이터베이스에 변경사항이 반영됨
  • 데이터 충돌의 가능성이 낮지만 발생할 경우 데이터 일관성을 보장해야 하는 시나리오에 적합

3. 데드락 (Deadlock)

  • 데드락은 두 개 이상의 트랜잭션이 서로의 락을 기다리면서 영원히 대기 상태에 빠지는 현상을 말함
    • 데드락(DeadLock) 또는 교착상태는 두 개 이상의 프로세스가 서로의 작업이 끝나기를 기다리는 '무한 대기 상태’
  • 각 트랜잭션이 다른 트랜잭션이 소유한 자원을 요구할 때 발생
  • 여러 트랜잭션이 서로의 자원을 기다리며 무한 대기 상태에 빠짐
  • 데드락은 다양한 형태의 자원에 대한 경쟁에서 발생할 수 있음
    - 자원은 주로 CPU 시간, 메모리, 파일, 장치 등을 포함

해결 방법

  • 데드락이 발생하지 않도록 예방하기
  • 데드락 발생 가능성을 인정하면서도 적절하게 회피하기
  • 데드락 발생을 허용하지만 데드락을 탐지하여, 데드락에서 회복하기
  • 트랜잭션 진행방향을 같은 방향으로 처리 (테이블A 업데이트 후 테이블B 업데이트, 블로킹)
  • 트랜잭션 처리속도를 최소화
  • SET LOCK_TIMEOUT: 잠금해제 시간 설정 ex) set lock_timeout 300
profile
안녕하세요. 토리입니다.

0개의 댓글