기본 개념
1. 동시성(Concurrency)
- 여러 작업이 동시에 실행될 수 있는 능력
- 티스레딩, 비동기 프로그래밍, 병렬 처리를 포함
2. 스레드(Tread)
- 스레드는 프로그램의 실행 단위
- 하나의 프로세스는 여러 개의 스레드를 가질 수 있으며, 각각의 스레드는 독립적으로 실행
3. 멀티스레딩 (Multithreading)
- 여러 스레드를 사용하여 여러 작업을 동시에 처리하는 기법
데이터 무결성의 종류
- 엔티티 무결성(Entity Integrity)
- 모든 인스턴스는 고유한 값 또는 NULL 값을 가지지 않는 속성이나 속성 그룹을 가져야 한다.
- 식별자(Identifier)에 의해서 지켜질 수 있다.
- 도메인 무결성(Domain Integrity)
- 엔터티의 특정 속성 값은 같은 데이터 타입과 길이, 같은 널 여부, 같은 기본 값, 같은 허용 값 등 동일한 범주의 값만이 존재해야 한다.
- 도메인 무결성은 기본 값이나 널 여부, 체크 조건 등으로 지켜질 수 있다.
- 참조 무결성(Referential Integrity)
- 엔터티의 외래 식별자 속성은 참조되는 엔터티의 주 식별자 값과 일치하거나 널(Null) 값이어야 한다.
- 참조 무결성은 외래 키 제약조건(foreign key constraint)에 의해서 지켜진다.
- 업무 무결성(Business Integrity)
- 기업에서 업무를 수행하는 방법이나 데이터를 처리하는 규칙을 의미한다.
- 업무 무결성을 물리적으로 강제하는 대표적인 방법에 트리거(Trigger)가 존재
트랜잭션 관리
제약 조건 설정
락 (Lock)
검증 로직
@Getter
@Entity
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private BigDecimal balance;
...
}
public class AccountService {
@PersistenceContext
private EntityManager entityManager;
@Transactional
public void updateBalance(Long accountId, BigDecimal amount) {
Account account = entityManager.find(Account.class, accountId);
BigDecimal newBalance = account.getBalance().add(amount);
account.setBalance(newBalance);
}
}
// 스레드 1
public void run() {
accountService.updateBalance(1L, new BigDecimal("100.00"));
}
// 스레드 2
public void run() {
accountService.updateBalance(1L, new BigDecimal("50.00"));
}
작업 순서
- 스레드 1이 Account 엔티티를 조회하여 잔액을 가져온다.
- 스레드 2도 같은 엔티티를 조회하여 잔액을 가져온다.
- 스레드 1이 새로운 잔액을 계산하고 설정한다.
- 스레드 2도 새로운 잔액을 계산하고 설정한다.
- 스레드 1이 트랜잭션을 커밋하여 데이터베이스에 반영한다.
- 스레드 2도 트랜잭션을 커밋하여 데이터베이스에 반영한다.
이 과정에서 스레드 2의 변경 사항이 스레드 1의 변경 사항을 덮어쓰게 된다.
스레드 1이 먼저 커밋되었으므로, 데이터베이스의 잔액은 1100.00이 된다.
결국 최종 잔액은 두 업데이트의 합이 아닌, 마지막으로 커밋된 값이 되어버린다.
-> Race Condition에 의한 데이터 불일치 문제 발생
@Transactional
public void updateBalance(Long accountId, BigDecimal amount) {
Account account = entityManager.find(Account.class, accountId, LockModeType.PESSIMISTIC_WRITE);
BigDecimal newBalance = account.getBalance().add(amount);
account.setBalance(newBalance);
}
@Entity
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private BigDecimal balance;
@Version
private Long version;
...
}
@Transactional
public void updateBalance(Long accountId, BigDecimal amount) {
Account account = entityManager.find(Account.class, accountId);
BigDecimal newBalance = account.getBalance().add(amount);
account.setBalance(newBalance);
// 버전 체크를 통한 Optimistic Locking
entityManager.flush(); // 트랜잭션이 커밋될 때 버전 번호를 확인하고, 충돌이 발생하면 예외를 던짐
}
Pessimistic Locking vs. Optimistic Locking
- 비관적 락 (Pessimistic Locking)
- 데이터에 접근할 때 즉시 잠금을 걸어 다른 트랜잭션이 접근하지 못하게 함.
- 충돌 가능성이 높을 때 사용
- 낙관적 락 (Optimistic Locking)
- 충돌이 드물다고 가정하고, 트랜잭션이 끝날 때 충돌을 감지
- 버전 필드를 사용하여 구현합니다. 충돌 가능성이 낮을 때 사용