본문 바로가기
데이터베이스 & 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 격리 수준 선택이유

  1. 데이터 일관성
    • 결제 처리 중 데이터 일관성 보장
    • Phantom Read 방지
  2. @Transactional(isolation = Isolation.REPEATABLE_READ) public PaymentResponse initiatePayment(PaymentRequest request) { // 트랜잭션 내에서 동일한 결제 데이터 보장 }
  3. 실제 적용 사례
    • 결제 상태 변경 시 동일 데이터 보장
    • 결제 금액 계산 시 중간 데이터 변경 방지

REPEATABLE READ와 함께 사용한 락 전략

  1. 비관적 락 사용
    • 결제 처리 시 비관적 락 적용
    • 동시 수정 방지
  2. @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("SELECT p FROM Payment p WHERE p.paymentId = :paymentId") Optional<Payment> findByPaymentIdWithLock(@Param("paymentId") String paymentId);
  3. 락 타임아웃 설정
    • 데드락 방지
    • 성능 고려
  4. @Query(value = "SELECT * FROM payments " + "WHERE status = 'PENDING' " + "FOR UPDATE SKIP LOCKED LIMIT 100", nativeQuery = true) List<Payment> findPendingPaymentsForProcessing();

REPEATABLE READ에서 발생할 수 있는 문제와 해결 방법

  1. 갭 락(Gap Lock) 문제
    • 인덱스 설계로 갭 락 최소화
    • 적절한 배치 크기 설정
  2. @Transactional(isolation = Isolation.REPEATABLE_READ) public void processSubscriptionPayment() { // 범위 락으로 인한 성능 저하 가능성 List<Payment> payments = paymentRepository .findByStatusWithLock(PaymentStatus.PENDING); }
  3. 해결 전략
    • 페이지네이션 적용
    • 시간 기반 필터링
  4. @Query(value = "SELECT * FROM payments " + "WHERE status = 'PENDING' " + "AND updated_at < :cutoffTime " + "ORDER BY created_at ASC " + "LIMIT :batchSize", nativeQuery = true) List<Payment> findStalePayments(...);

트랜잭션 격리 수준과 성능 사이의 균형 전략

  1. 선택적 락 사용
    • 중요 트랜잭션만 강한 격리 수준 적용
    • 조회는 읽기 전용 트랜잭션 사용
  2. // 중요 결제 처리 @Lock(LockModeType.PESSIMISTIC_WRITE) // 일반 조회 @Transactional(readOnly = true)
  3. 성능 최적화
    • 인덱스 활용
    • 적절한 조회 조건 설정
  4. @Query("SELECT p FROM Payment p " + "WHERE p.status = :status " + "AND p.updatedAt < :cutoffTime")

REPEATABLE READ와 결제 시스템의 신뢰성 관계

  1. 데이터 정합성
    • 결제 처리 중 데이터 일관성
    • 중복 결제 방지
  2. @Transactional(isolation = Isolation.REPEATABLE_READ) public void processPayment(String paymentId) { Payment payment = findPaymentWithLock(paymentId); // 트랜잭션 내에서 결제 금액, 상태 등 일관성 보장 }
  3. 오류 처리
    • 롤백을 통한 안전성 보장
    • 실패 시 일관성 유지
  4. try { // 결제 처리 transactionManager.commit(status); } catch (Exception e) { transactionManager.rollback(status); throw new PaymentException("결제 처리 실패", e); }

REPEATABLE READ 사용 시 고려한 모니터링 전략

  1. 트랜잭션 로깅
    • 트랜잭션 실행 시간 추적
    • 락 경합 상황 모니터링
  2. 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); }
  3. 성능 메트릭
    • 트랜잭션 처리 시간
    • 락 대기 시간
    • 롤백 빈도

'데이터베이스 & ORM' 카테고리의 다른 글

비관적 락(Pessimistic Lock)  (1) 2024.11.22
PostgreSQL & pgvector  (0) 2024.11.16
Row-Level Lock  (1) 2024.11.12
VACUUM ANALYZE  (1) 2024.11.11
PostgreSQL GIN  (5) 2024.11.10