[SpringBoot] 결제 로직(5) - 트랜잭션 분리에 따른 롤백

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

springboot-결제로직

목록 보기
5/6

이전 게시글에서 데드락을 방지하기 위해 REQUIRED_NEW 를 이용해 트랜잭션을 분리하였습니다.
이번에는 이에 대한 롤백 방식에 대해 작성해 보고자 합니다.

롤백이란?

롤백(Rollback)이란 트랜잭션 처리 중 오류가 발생했을 때, 해당 트랜잭션 범위 내의 모든 작업을 취소하고 데이터베이스를 이전 상태로 되돌리는 것을 말합니다. 기본적으로 @Transactional 어노테이션이 붙어 있는 경우, 예외가 발생하면 자동으로 해당 트랜잭션 범위 내에서 롤백이 이루어집니다.

@Transactional
public void test() {
	Test test1 = new Test();
	testRepository.save(test1); 
    
    Test test2 = new Test();
    testRepository.save(test2); 
}

test1을 저장한 후 test2를 저장하는 과정에서 오류가 발생하면, test1이 롤백됩니다.
이는 test1test2같은 트랜잭션 범위에 있기 때문입니다.

하지만, 아래와 같은 코드에서 TestService.save() 메소드를 진행하는 과정에서 오류가 발생하면 어떻게 될까요?

@Transactional
public void test() {
	Test1 test1 = new Test();
    testRepository.save(test1);
    
    testService.save();
}

// TestService
class TestService {
	
    @Transactional(propagation = Propagation.REQUIRED_NEW)
    public void save() {
    	Test2 test2 = new Test();
        testRepository.save(test2);
    }
}

test() 메소드와 TestService.save() 메소드는 서로 다른 트랜잭션 범위에 있기 때문에 test() 메소드의 내용은 롤백되지 않습니다.

저는 이러한 문제를 해결하기 위해 주로 MSA에서 사용하는 Saga 패턴과 유사하게 처리하였습니다.


Saga 패턴이란?

  • Saga 패턴은 주로 MSA 서비스에서 데이터의 일관성을 관리하는 방법입니다.
  • 각 서비스는 로컬 트랜잭션을 가지고 있으며, 해당 서비스 데이터를 업데이트하여 메시지 또는 이벤트를 발생해서, 다음 단계 트랜잭션을 호출하게 됩니다.
  • 만약, 해당 프로세스가 실패하면 데이터 정합성을 맞추기 위해 이전 트랜잭션에 대해 보상 트랜잭션을 실행합니다.

보상 트랜잭션이란?
보상 트랜잭션(Compensating Transaction)은 분산 트랜잭션 시스템에서 특정 작업이 실패했을 때 성공한 작업들을 취소하여 시스템의 일관성을 유지하기 위해 사용되는 트랜잭션입니다.


결제 로직에서 보상 트랜잭션을 적용한 방법

3개의 상품(상품 ID : 1, 2, 3)을 결제하는 예시로 설명하겠습니다.

상품1에 대한 재고량 감소 작업이 완료되면 transactionMap에는 (Key: 1, Value: 재고량)이 적재됩니다.
이때 상품 2에 대한 재고량 감소 과정에서 오류가 발생하면 try ~ catch 블록에 의해 rollbackProductQuantity 메소드가 실행됩니다. 이 메소드에서는 상품에 대해 재고량을 다시 증가시켜 주는 작업을 합니다. 이 과정을 통해 상품 재고량에 대한 일관성을 유지합니다.

만약 모든 상품에 대한 재고량 감소 작업이 완료되면 transactionMap에는 3개의 상품ID - 재고량 데이터가 적재됩니다. 이때 결제 승인 API 호출 과정에서 오류가 발생하면 3개의 상품에 대해 재고량을 다시 증가시켜줍니다.

/**
 * 결제 승인 API
 */
@PostMapping("/confirm")
public ResponseEntity<ApiResponseEntity<String>> confirm(@Valid @RequestBody UserPayReqDto.VerifyPayment dto) {
    Map<Long, Integer> transactionMap = new HashMap<>();
    TossPaymentPayTransaction tossPaymentPayTransaction;

    try {
        // 재고량 확인 및 수정
        tossPaymentPayTransaction = payConfirmService.modifyProductStockQuantity(transactionMap, dto.orderId());
    } catch (Exception e) {
        log.error("재고량 확인 및 수정 실패 :: ", e);
        rollbackProductQuantity(transactionMap);
        throw new PayApiException();
    }

    try {
        // 결제 승인 API 호출
        payConfirmService.payConfirm(tossPaymentPayTransaction, dto);
    } catch (Exception e) {
        log.error("결제 승인 API 호출 실패 :: ", e);
        rollbackProductQuantity(transactionMap);
        throw new PayApiException();
    }

    // 장바구니 정보 삭제(비동기)
    payConfirmService.removeShoppingCart(dto.orderId());

    return ResponseEntity.ok(ApiResponseEntity.of(ResponseText.SUCCESS_PAY));
}

/**
 * 재고량 복원
 */
private void rollbackProductQuantity(Map<Long, Integer> transactionMap) {
    for (Entry<Long, Integer> entry : transactionMap.entrySet()) {
        userProductService.increaseProductQuantityWithLock(entry.getKey(), entry.getValue());
    }
}

만약, 재고량을 복원하는 과정에서 오류가 발생할 가능성이 있으므로, Retry를 통해 반복 처리하거나 관리자에게 알림을 전송하는 등의 추가 조치가 필요할 것 같습니다.


참고 자료

https://velog.io/@hgs-study/saga-1

profile
주경야독

0개의 댓글