1. Row-Level Lock 개념
Row-Level Lock은 데이터베이스에서 동시성 제어를 위해 개별 행(row) 단위로 잠금을 수행하는 메커니즘
1.1 Lock 종류
-- 1. Exclusive Lock (X-Lock)
SELECT * FROM payments
WHERE payment_id = 'P123' FOR UPDATE;
-- 2. Shared Lock (S-Lock)
SELECT * FROM payments
WHERE payment_id = 'P123' FOR SHARE;
-- 3. 조건부 Lock
SELECT * FROM payments
WHERE payment_id = 'P123' FOR UPDATE SKIP LOCKED;
1.2 JPA에서의 사용
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM Payment p WHERE p.paymentId = :paymentId")
Optional<Payment> findByPaymentIdWithLock(@Param("paymentId") String paymentId);
2. 실무 적용 사례
2.1 결제 처리 시스템
@Service
@Transactional
public class PaymentService {
@Autowired
private PaymentRepository paymentRepository;
public void processPayment(String paymentId) {
// 비관적 락으로 결제 정보 조회
Payment payment = paymentRepository.findByIdWithPessimisticLock(paymentId)
.orElseThrow(() -> new PaymentNotFoundException(paymentId));
// 동시 처리 방지된 상태에서 결제 처리
if (payment.getStatus() == PaymentStatus.PENDING) {
payment.setStatus(PaymentStatus.PROCESSING);
processPaymentLogic(payment);
}
}
}
2.2 재고 관리 시스템
@Service
public class InventoryService {
@Transactional
public void decreaseStock(Long productId, int quantity) {
// 비관적 락으로 재고 조회
Product product = productRepository.findByIdWithPessimisticLock(productId)
.orElseThrow(() -> new ProductNotFoundException(productId));
// 재고 차감 로직
if (product.getStock() >= quantity) {
product.decreaseStock(quantity);
} else {
throw new InsufficientStockException();
}
}
}
2.3 Lock Timeout 처리
@Configuration
public class JpaConfig {
@Bean
public JpaProperties jpaProperties() {
JpaProperties props = new JpaProperties();
props.getProperties().put("javax.persistence.lock.timeout", "3000");
return props;
}
}
3. 성능 최적화 전략
3.1 Lock 범위 최소화
// 좋은 예시
@Query("SELECT p FROM Payment p WHERE p.id = :id AND p.status = 'PENDING'")
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Payment> findPendingPaymentById(@Param("id") Long id);
// 피해야 할 예시
@Query("SELECT p FROM Payment p WHERE p.status = 'PENDING'")
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Payment> findAllPendingPayments();
3.2 Dead Lock 방지
@Transactional
public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
// 항상 작은 ID의 계좌를 먼저 잠금
Long firstLockId = Math.min(fromAccountId, toAccountId);
Long secondLockId = Math.max(fromAccountId, toAccountId);
Account firstAccount = accountRepository.findByIdWithLock(firstLockId);
Account secondAccount = accountRepository.findByIdWithLock(secondLockId);
// 송금 로직 수행
}
Row-Level Lock과 Table-Level Lock의 차이점과 각각의 사용 시나리오
- Lock 범위
- Row-Level Lock: 개별 행 단위로 잠금
- Table-Level Lock: 테이블 전체를 잠금
- 동시성
- Row-Level Lock: 다른 행에 대한 동시 처리 가능
- Table-Level Lock: 테이블 전체가 잠기므로 동시성 제한
- // Row-Level Lock 예시 @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("SELECT p FROM Payment p WHERE p.id = :id") Optional<Payment> findByIdWithLock(@Param("id") Long id);
- 사용 시나리오
- Row-Level Lock: 결제 처리, 재고 관리 등 개별 레코드 단위의 동시성 제어
- Table-Level Lock: 스키마 변경, 대량 데이터 업데이트 등
Optimistic Lock과 Pessimistic Lock의 차이점과 선택 기준
- 동작 방식
- Optimistic: 버전 관리를 통한 충돌 감지
- Pessimistic: 실제 DB 수준의 Lock 획득
- // Optimistic Lock @Version private Long version; // Pessimistic Lock @Lock(LockModeType.PESSIMISTIC_WRITE)
- 성능 특성
- Optimistic: Lock 오버헤드 없음, 충돌 시 재시도 필요
- Pessimistic: Lock 획득 오버헤드, 충돌 방지 보장
- 선택 기준
- Optimistic: 충돌이 적은 경우, 읽기가 많은 경우
- Pessimistic: 충돌이 빈번한 경우, 데이터 정합성이 중요한 경우
Dead Lock을 방지 전략
- Lock 획득 순서 정규화
- @Transactional public void processTwoAccounts(Long acc1Id, Long acc2Id) { // 항상 작은 ID부터 Lock 획득 Long firstId = Math.min(acc1Id, acc2Id); Long secondId = Math.max(acc1Id, acc2Id); // Lock 획득 Account first = accountRepo.findByIdWithLock(firstId); Account second = accountRepo.findByIdWithLock(secondId); }
- Lock Timeout 설정
- spring.jpa.properties.javax.persistence.lock.timeout=5000
- Lock 범위 최소화
- 필요한 레코드만 Lock
- Transaction 시간 최소화
- Dead Lock 감지 및 로깅
- Dead Lock 모니터링 구현
- 발생 시 자동 알림
Row-Level Lock 성능 이슈와 해결 방법
- Lock 경합 문제
- // 해결책: SKIP LOCKED 사용 @Query(value = "SELECT * FROM payments WHERE status = 'PENDING' " + "FOR UPDATE SKIP LOCKED LIMIT 100", nativeQuery = true) List<Payment> findPendingPaymentsForProcessing();
- Transaction 시간 최적화
- @Transactional public void processPayment(String paymentId) { // 사전 검증 if (!paymentValidator.isValid(paymentId)) { return; } // Lock 획득 후 최소한의 작업만 수행 Payment payment = paymentRepository.findByIdWithLock(paymentId); payment.process(); }
- 배치 처리 도입
- 대량 처리 시 벌크 연산 활용
- 페이지네이션 적용
Row-Level Lock의 격리 수준(Isolation Level)과의 관계
- 격리 수준별 Lock 동작
- @Transactional(isolation = Isolation.REPEATABLE_READ) public void processWithLock() { // REPEATABLE_READ에서의 Lock 동작 }
- 주요 고려사항
- READ_COMMITTED: 기본적인 Row-Level Lock 지원
- REPEATABLE_READ: Range Lock 추가 발생 가능
- SERIALIZABLE: 가장 엄격한 Lock 적용
- 실무 적용
- 대부분 READ_COMMITTED 사용
- 특수한 경우만 상위 격리 수준 적용
낙관적 락과 비관적 락을 혼용해서 사용한 경험
- 사용 사례
- @Entity public class Product { @Version private Long version; // 일반적인 재고 관리용 @Lock(LockModeType.PESSIMISTIC_WRITE) public void criticalUpdate() { // 중요 업데이트용 // 로직 } }
- 적용 전략
- 일반 업데이트: 낙관적 락
- 결제 처리: 비관적 락
- 재고 관리: 상황에 따라 선택
- 이점
- 유연한 동시성 제어
- 성능과 안정성 균형
'데이터베이스 & ORM' 카테고리의 다른 글
PostgreSQL & pgvector (0) | 2024.11.16 |
---|---|
REPEATABLE READ (0) | 2024.11.15 |
VACUUM ANALYZE (1) | 2024.11.11 |
PostgreSQL GIN (5) | 2024.11.10 |
QueryDSL 개요 (0) | 2024.07.25 |