[SpringBoot] 결제 로직(3) - REQUIRES_NEW

포테이토웅·2024년 7월 20일
0

springboot-결제로직

목록 보기
3/6

🎇 이슈 발생

저는 여러 개의 상품을 한 번에 결제할 수 있게 기획하였습니다. orderId를 통해 결제 트랜잭션 정보 조회 후, 상품의 종류 만큼 userProductService.decreaseProductQuantityWithLock 메소드를 호출하여 재고량을 조정하였습니다.

// PayConfirmService
@Transactional
public TossPaymentPayTransaction modifyProductStockQuantity(Map<Long, Integer> transactionMap, final String orderId) {
    // 결제 트랜잭션 정보 조회
    TossPaymentPayTransaction tossPaymentPayTransaction = tossPaymentPayTransactionRepository.findTossPaymentPayTransactionAndPayTransactionAndProductByOrderId(orderId)
        .orElseThrow(() -> new CustomException(ErrorCode.PAY_TRANSACTION_NOT_FOUND));

    // 상품 재고량 수정
    List<PayTransaction> payTransactions = tossPaymentPayTransaction.getPayTransactions();
    for (PayTransaction payTransaction : payTransactions) {
        final long productId = payTransaction.getProduct().getProductId();
        userProductService.decreaseProductQuantityWithLock(productId, payTransaction.getQuantity());
        transactionMap.put(productId, payTransaction.getQuantity());
    }
    return tossPaymentPayTransaction;
}

해당 메소드 또한 동시성에 대한 테스트를 진행해보았습니다. userProductService.decreaseProductQuantityWithLock 에서 베타락을 이용해 동시성을 제어하고 있으므로 당연히 테스트가 통과할 줄 알았습니다.

하지만... 데드락이 발생하였습니다.


❓ 데드락(Deadlock)이란?

데드락(교착상태)
둘 이상의 프로세스가 다른 프로세스가 점유하고 있는 자원을 서로 기다릴 때 무한 대기에 빠지는 상황을 의미합니다.

데드락의 발생조건

  • 상호 배제 : 한 번에 프로세스 하나만 해당 자원을 사용할 수 있다.
  • 점유 대기 : 자원을 최소한 하나 보유하고, 다른 프로세스에 할당된 자원을 점유하기 위해 대기하는 프로세스가 존재애햐 한다.
  • 비선점 : 이미 할당된 자원을 강제로 빼앗을 수 없다.
  • 순환 대기 : 대기 프로세스의 집합이 순환 형태로 자원을 대기하고 있어야 한다.

❓ 왜 데드락에 빠졌을까..?

Propagation.REQUIRED

@Transactional 어노테이션의 기본 전파 레벨은 REQUIRED입니다.

REQUIRED
부모 트랜잭션이 존재한다면 부모 트랜잭션에 합류하고, 그렇지 않으면 새로운 트랜잭션을 만듭니다.

아래 Flow Chart는 3개의 상품을 한 번에 결제하는 예시입니다. 마지막 상품까지 재고량 수정이 완료되어야 트랜잭션이 종료됩니다. 즉, 트랜잭션이 종료될 때까지 각 상품들에 대한 베타락이 종료되지 않습니다.

두 명의 사용자가 같은 상품을 결제하는 경우에 대한 예시를 살펴보겠습니다.

  1. 사용자 A가 상품1에 베타락을 걸고 재고량을 수정합니다.
  2. 사용자 B가 상품2에 베타락을 걸고 재고량을 수정합니다.
  3. 사용자 A가 상품2에 대한 베타락을 획득하려고 할 때, 사용자 B의 트랜잭션이 종료되지 않아 대기 상태에 빠집니다.
  4. 사용자 B가 상품1에 대한 베타락을 획득하려고 할 때, 사용자 A의 트랜잭션이 종료되지 않아 대기 상태에 빠집니다.

이 과정에서 순환 대기가 발생해 데드락이 발생합니다.


📖 트러블 슈팅

저는 트랜잭션의 범위가 너무 커서 데드락이 발생한다고 생각했습니다. 트랜잭션을 어떻게 분리할지 고민한 끝에, 전파 레벨을 수정하기로 했습니다.

Propagation.REQUIRED_NEW

REQUIRED_NEW
무조건 새로운 트랜잭션을 만듭니다. Nested한 방식으로 메소드 호출이 이루어지더라도 rollback은 각각 이루어집니다.

이렇게 트랜잭션의 전파 레벨을 REQUIRED_NEW로 변경하여 트랜잭션을 분리하였습니다.
이제 사용자 A가 상품2에 대한 베타락을 획득하려고 할 때, 사용자 B가 상품2를 수정중이더라도 트랜잭션이 금방 종료되어 베타락을 획득할 수 있으므로 순환 대기가 발생하지 않습니다.

// UserProductService
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void decreaseProductQuantityWithLock(final long productId, final int quantity) {
    Product product = productRepository.findByIdWithLock(productId)
            .orElseThrow(() -> new CustomException(ErrorCode.PRODUCT_NOT_FOUND));

    // 재고량 검증
    if (quantity > product.getStockQuantity()) {
        throw new CustomException(ErrorCode.PRODUCT_STOCK_NOT_ENOUGH);
    }

    product.decreaseStockQuantity(quantity);
    productRepository.save(product);
}

📗 참고 자료

https://deious.tistory.com/297
https://devlog-wjdrbs96.tistory.com/424

profile
주경야독

0개의 댓글