데이터베이스 & ORM
REPEATABLE READ
by DoRightting
2024. 11. 15.
1. REPEATABLE READ
1.1 결제 서비스에서의 사용
@Service
@RequiredArgsConstructor
public class KakaoPayService {
@Transactional(isolation = Isolation.REPEATABLE_READ)
public PaymentResponse initiatePayment(PaymentRequest request) {
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
TransactionStatus status = transactionManager.getTransaction(def);
try {
Payment payment = createPayment(paymentId, orderId, request, kakaoResponse);
paymentRepository.save(payment);
savePaymentLog(payment, "READY", "결제 준비");
PaymentMessage message = createPaymentMessage(payment);
paymentProducer.sendPaymentMessage(message);
transactionManager.commit(status);
return createPaymentResponse(payment, kakaoResponse);
} catch (Exception e) {
transactionManager.rollback(status);
throw new PaymentException("결제 준비 중 오류가 발생했습니다.", e);
}
}
}
1.2 결제 조회 시 락 사용
@Repository
public interface PaymentRepository extends JpaRepository<Payment, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM Payment p WHERE p.paymentId = :paymentId")
Optional<Payment> findByPaymentIdWithLock(@Param("paymentId") String paymentId);
@Lock(LockModeType.PESSIMISTIC_READ)
@Query("SELECT p FROM Payment p WHERE p.status = :status")
List<Payment> findByStatusWithLock(@Param("status") PaymentStatus status);
}
1.3 구독 서비스에서의 사용
@Service
public class SubscriptionService {
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void processPaymentRetry(String subscriptionId) {
Subscription subscription = subscriptionRepository
.findBySubscriptionIdWithLock(subscriptionId)
.orElseThrow(() -> new PaymentException("구독 정보를 찾을 수 없습니다."));
try {
processSubscriptionPayment(subscription);
} catch (Exception e) {
handlePaymentFailure(subscription);
throw e;
}
}
}
REPEATABLE READ 격리 수준 선택이유
- 데이터 일관성
- 결제 처리 중 데이터 일관성 보장
- Phantom Read 방지
- @Transactional(isolation = Isolation.REPEATABLE_READ) public PaymentResponse initiatePayment(PaymentRequest request) { // 트랜잭션 내에서 동일한 결제 데이터 보장 }
- 실제 적용 사례
- 결제 상태 변경 시 동일 데이터 보장
- 결제 금액 계산 시 중간 데이터 변경 방지
REPEATABLE READ와 함께 사용한 락 전략
- 비관적 락 사용
- 결제 처리 시 비관적 락 적용
- 동시 수정 방지
- @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("SELECT p FROM Payment p WHERE p.paymentId = :paymentId") Optional<Payment> findByPaymentIdWithLock(@Param("paymentId") String paymentId);
- 락 타임아웃 설정
- @Query(value = "SELECT * FROM payments " + "WHERE status = 'PENDING' " + "FOR UPDATE SKIP LOCKED LIMIT 100", nativeQuery = true) List<Payment> findPendingPaymentsForProcessing();
REPEATABLE READ에서 발생할 수 있는 문제와 해결 방법
- 갭 락(Gap Lock) 문제
- 인덱스 설계로 갭 락 최소화
- 적절한 배치 크기 설정
- @Transactional(isolation = Isolation.REPEATABLE_READ) public void processSubscriptionPayment() { // 범위 락으로 인한 성능 저하 가능성 List<Payment> payments = paymentRepository .findByStatusWithLock(PaymentStatus.PENDING); }
- 해결 전략
- @Query(value = "SELECT * FROM payments " + "WHERE status = 'PENDING' " + "AND updated_at < :cutoffTime " + "ORDER BY created_at ASC " + "LIMIT :batchSize", nativeQuery = true) List<Payment> findStalePayments(...);
트랜잭션 격리 수준과 성능 사이의 균형 전략
- 선택적 락 사용
- 중요 트랜잭션만 강한 격리 수준 적용
- 조회는 읽기 전용 트랜잭션 사용
- // 중요 결제 처리 @Lock(LockModeType.PESSIMISTIC_WRITE) // 일반 조회 @Transactional(readOnly = true)
- 성능 최적화
- @Query("SELECT p FROM Payment p " + "WHERE p.status = :status " + "AND p.updatedAt < :cutoffTime")
REPEATABLE READ와 결제 시스템의 신뢰성 관계
- 데이터 정합성
- @Transactional(isolation = Isolation.REPEATABLE_READ) public void processPayment(String paymentId) { Payment payment = findPaymentWithLock(paymentId); // 트랜잭션 내에서 결제 금액, 상태 등 일관성 보장 }
- 오류 처리
- 롤백을 통한 안전성 보장
- 실패 시 일관성 유지
- try { // 결제 처리 transactionManager.commit(status); } catch (Exception e) { transactionManager.rollback(status); throw new PaymentException("결제 처리 실패", e); }
REPEATABLE READ 사용 시 고려한 모니터링 전략
- 트랜잭션 로깅
- 트랜잭션 실행 시간 추적
- 락 경합 상황 모니터링
- private void savePaymentLog(Payment payment, String type, String content) { PaymentLog log = PaymentLog.builder() .payment(payment) .logType(type) .content(content) .createdAt(LocalDateTime.now()) .build(); paymentLogRepository.save(log); }
- 성능 메트릭