[식구하자_MSA] Saga패턴을 이용한 분산 트랜잭션 제어(쿠폰 적용+결제 프로세스)

이민우·2024년 5월 19일
2

🍀 식구하자_MSA

목록 보기
13/21

이번 포스팅은 식구하자 프로젝트 msa상에서 분산 트랜잭션을 제어하는 방법 중 하나인 saga-pattern을 활용한 과정을 포스팅 해보겠습니다.

1. 분산 트랜잭션


💡 분산 트랜잭션을 왜 제어해야하는가?

아래 그림은 결제 프로세스를 MSA 분산 환경으로 간략하게 나타냈습니다. 각 도메인은 각각의 DB를 바라보고 있으며, 결제 프로세스는 2개의 서비스(plant-coupon-service, plant-pay-service)와 DB를 거쳐야 완료됩니다.
문제없이 항상 완료만 되는 상황이면 좋겠지만, 쿠폰 마이크로서비스에서 쿠폰을 사용해서 DB에 저장한 후에, Payment 마이크로서비스에서 예외처리 혹은 장애가 나서 롤백해야한다면 어떻게 처리해야할까요?

2. Saga Pattern


❓ Saga Pattern이란?

Saga Pattern은 마이크로 서비스에서 데이터 일관성을 관리하는 방법입니다.
각 서비스는 로컬 트랜잭션을 가지고 있으며, 해당 서비스 데이터를 업데이트하며 메시지 또는 이벤트를 발행해서, 다음 단계 트랜잭션을 호출하게 됩니다.
만약, 해당 프로세스가 실패하게 되면 데이터 정합성을 맞추기 위해 이전 트랜잭션에 대해 보상 트랜잭션을 실행합니다.
NoSQL 같이 분산 트랜잭션 처리를 지원하지 않거나, 각기 다른 서비스에서 다른 DB 밴더사를 이용할 경우에도 Saga Pattenrn을 이용해서 데이터 일관성을 보장 받을 수 있습니다.

❓ Saga Pattern종류


Orchestration based SAGA pattern

Orchestration-Based Saga 패턴은 트랜잭션 처리를 위해 Saga 인스턴스(Manager)가 별도로 존재합니다. 트랜잭션에 관여하는 모든 App은 Manager에 의해 점진적으로 트랜잭션을 수행하며 결과를 Manager에게 전달하게 되고, 비즈니스 로직상 마지막 트랜잭션이 끝나면 Manager를 종료해서 전체 트랜잭션 처리를 종료합니다.
만약 중간에 실패하게 되면 Manager에서 보상 트랜잭션을 발동하여 일관성을 유지합니다.

해당 Orchestration-Based Saga 패턴은 모든 관리를 Manager가 호출하기 때문에 분산트랜잭션의 중앙 집중화가 이루어집니다.

장점

  • 서비스간의 복잡성이 줄어들어서 구현 및 테스트가 쉬워집니다.
  • 트랜잭션의 현재 상태를 Manager가 알고 있으므로 롤백을 하기 쉽습니다.

단점

  • 관리를 해야하는 Orchestrator 서비스가 추가되어야하기 때문에 인프라 구현이 복잡해집니다.

Choreography 방식


  • Choreography 방식은 서비스끼리 직접적으로 통신하지 않고, 이벤트 Pub/Sub을 활용해서 통신하는 방식입니다.
  • 프로세스를 진행하다가 여러 서비스를 거쳐 서비스(Stock, Payment)에서 실패(예외처리 혹은 장애)가 난다면 보상 트랜잭션 이벤트를 발행합니다.
  • 장점으론, 간단한 workflow에 적합하며 추가 서비스 구현 및 유지관리가 필요하지 않아서 간단하게 세팅할 수 있습니다.
  • 단점으론, 트랜잭션을 시뮬레이션하기 위해 모든 서비스를 실행해야하기 때문에 통합테스트와 디버깅이 어려운 점이 있습니다.

📌 Choreography 방식 선택

저의 프로젝트 분산 트랙잰셕은 workflow가 간단하며, 추가적인 Orchestration-Based Saga을 위한 Manager 추가 서비스 구현보다는 Choreography 방식을 선택했습나다!

3. 결제 분산 트랜잭션 구성


✅ 정상적인 분산 트랜잭션 프로세스

  1. 쿠폰 사용 요청
  2. 쿠폰 마이크로서비스에서 쿠폰 사용 이벤트 발행
  3. 결제 마이크로서비스에서 쿠폰 사용 이벤트 구독하고
  4. 결제 메서드 실행
  5. 결제 성공

❌ 실패 분산 트랜잭션 프로세스


1. 결제 마이크로 서비스에서 트랜잭션 실패
2. 결제 마이크로 서비스에서 쿠폰 사용 롤백 이벤트 발행
3. 쿠폰 마이크로 서비스에서 쿠폰 사용 롤백 이벤트 구독
4. 쿠폰 사용 취소

4. 결제 분산 트랜잭션 적용 코드


kafka 설정 파일 및 참고 코드가 궁금하신 분은 GitHub을 참고해주세요.
Github 주소 : https://github.com/LminWoo99/PlantBackend/tree/msa-master

CouponService.java

    /**
     * 쿠폰 사용 메서드
     * 쿠폰 , 결제 마이크로서비스 saga pattern 적용
     * coupon status가 미적용이면 바로 결제 이벤트
     * 적용이면 쿠폰 사용후 결제 이벤트
     * @param : PaymentRequestDto paymentRequestDto
     */
   @Transactional
    public void useCouponAndPayment(PaymentRequestDto paymentRequestDto) {
       if (paymentRequestDto.getCouponStatus()== CouponStatus.쿠폰미사용){
           log.info("======couponUseConsumer Data :{}======", paymentRequestDto);
           paymentProducer.create(paymentRequestDto);
           return;
       }
        Coupon coupon = couponRepository.findByMemberNoAndCouponNo(paymentRequestDto.getMemberNo(), paymentRequestDto.getCouponNo());
        //사용완료 및 변경감지
        coupon.useCoupon();
       log.info("======couponUseConsumer Data :{}======", paymentRequestDto);
        //쿠폰 적용완료후 결제 요청
        paymentProducer.create(paymentRequestDto);
   }
    /**
     * 쿠폰 롤백 메서드
     * 보상 트랜잭션
     * 결제 실패시 쿠폰 상태를 update
     * @param : PaymentRequestDto paymentRequestDto
     */
    @Transactional
    public void revertCouponStatus(PaymentRequestDto paymentRequestDto) {
        Coupon coupon = couponRepository.findByMemberNoAndCouponNo(paymentRequestDto.getMemberNo(), paymentRequestDto.getCouponNo());
        if (coupon.getType() == CouponStatusEnum.사용완료) {
            //다시 쿠폰 상태 롤백 및 변경감지
            coupon.revertCoupon();
        }
    }
  • revertCouponStatus는 결제 서비스에서 롤백시 사용 상태를 롤백

PaymentProducer.java

@Component
@RequiredArgsConstructor
@Slf4j
public class PaymentProducer {
    private final KafkaTemplate<String, PaymentRequestDto> kafkaTemplate;


    public void create(PaymentRequestDto paymentRequestDto) {
        log.info("쿠폰 사용 이벤트: {}", paymentRequestDto.getCouponNo());
        kafkaTemplate.send("payment", paymentRequestDto);
    }


}
  • 쿠폰 적용이 완료됐으면, 결제 마이크로서비스로 payment 이벤트 발행

CouponRollbackConsumer.java

@Service
@Slf4j
@RequiredArgsConstructor
public class CouponRollbackConsumer {
    private final CouponService couponService;
    @KafkaListener(topics = "coupon-rollback", containerFactory = "couponUseListenerContainerFactory")
    public void handleCouponRollbackEvent(PaymentRequestDto event) {
        log.info("======보상 트랜잭션 동작 , 쿠폰 번호 :{} =====" ,event.getCouponNo());
        couponService.revertCouponStatus(event);
    }
}

PaymentService.java

 /**
     *
     * 식구페이 거래 메서드
     * 판매자 상대 멤버 번호를 통해 해당 조회 후
     * 판매자 paymoney += 거래할 금액
     * 구매자 Paymoney -= 거래할 금액
     * 분산 트랜잭션 Saga Pattern 적용
     * 에러 발생시 쿠폰 마이크로서비스로 보상 트랜잭션 시작, Rollback
     * @param : PaymentRequestDto paymentRequestDto, Integer sellerNo
     */
    @Transactional
    public void tradePayMoney(PaymentRequestDto paymentRequestDto) {
        try {
            Payment buyerPayment = paymentRepository.findByMemberNo(paymentRequestDto.getMemberNo());
            Integer buyerPayMoney = paymentRequestDto.getPayMoney();

            if (paymentRequestDto.getCouponStatus() == CouponStatus.쿠폰사용) {
                //쿠폰 사용시 구매자 결제정보만 쿠폰금액 차감
                buyerPayMoney += paymentRequestDto.getDiscountPrice();
            }
//            errorPerHalf();
            paymentRepository.tradePayMoney(paymentRequestDto.getSellerNo(), buyerPayment.getMemberNo(), paymentRequestDto, buyerPayMoney);
        } catch (CustomException e) {
            // 결제 실패 시 쿠폰 사용 취소 이벤트 발행
            log.error("===[결제 요청 오류] -> coupon-rollback ,  쿠폰 번호 :{} / {}====",paymentRequestDto.getCouponNo(), e.getMessage());
            couponRollbackProducer.rollbackCouponStatus(paymentRequestDto);

        }

    }
트랜잭션 실패시 쿠폰 사용 취소 이벤트 발행(보상 트랜잭션)

CouponRollbackProducer.java

@Component
@RequiredArgsConstructor
public class CouponRollbackProducer {
    private final KafkaTemplate<String, PaymentRequestDto> kafkaTemplate;
    public void rollbackCouponStatus(PaymentRequestDto couponRequestDto) {
        kafkaTemplate.send("coupon-rollback", couponRequestDto);
    }

}

5. 보상 트랜잭션 실습

Payment 마이크로 서비스에서 트랜잭션이 실패해서 보상 트랜잭션 발생상황을 테스트 해보도록 하겠습니다!

기대 (1) : Payment 서비스에서 쿠폰 롤백 이벤트 발행
기대 (2) : Coupon 서비스에서 쿠폰 롤백 이벤트 구독 후 쿠폰 롤백

✅ 1. 결제 서비스에서 쿠폰 롤백 이벤트 발행

    private void errorPerHalf() {
        int zeroOrOne = new Random().nextInt(BETWEEN_ZERO_AND_ONE);

        if (zeroOrOne == 0) {
            throw ErrorCode.paymentProcessingError();
        }
    }
  1. errorPerHalf() 메서드로 50% 확률로 CustomException을 발생
  2. 트랜잭션 실패로 인해 catch문의 rollbackCouponStatus() 메서드 실행
  3. 카프카 브로커에 coupon-rollback 토픽에서 보상 트랜잭션 실행

✅ 2. 쿠폰 서비스에서 쿠폰 상태 롤백 이벤트 구독 후 쿠폰 롤백

보상 트랜잭션 : 결제 서비스에서 트랜잭션이 실패해서 쿠폰 상태 롤백

GitHub Repository : https://github.com/LminWoo99/PlantBackend/tree/msa-master
참고 : https://azderica.github.io/01-architecture-msa/
https://csy7792.tistory.com/349

profile
백엔드 공부중입니다!

0개의 댓글

관련 채용 정보