락 이란
- 락(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