예약으로 이어지지 않은 성공적인 결제

고객이 결제했습니다. Razorpay는 성공을 표시했습니다. 웹훅은 HTTP 200을 보냈습니다. 결제는 완료(captured)되었습니다.

하지만 예약은 "확인 중(confirming)" 상태에 멈춰 있었습니다.

오류는 나타나지 않았습니다. 코드를 깨뜨리는 예외도 없었습니다. 알림도 울리지 않았습니다. 모든 지표는 시스템이 정상임을 나타냈습니다.

하지만 고객은 아무것도 받지 못했습니다. 크리에이터에게는 예약이 생성되지 않았습니다.

돈을 받는 것은 쉽습니다. 모든 결제가 예약으로 이어지도록 보장하는 것이 진짜 과제입니다.

대부분의 튜토리얼은 다음과 같은 흐름을 제안합니다:

이는 위험합니다. 비즈니스 로직이 웹훅 내부에 있으면, 전달 성공 여부에 전적으로 의존하게 됩니다. 웹훅은 재시도, 중복, 부분적 실패 문제에 직면합니다.

우리는 이러한 작업을 분리하기 위해 아키텍처를 변경했습니다. 이제 웹훅은 이벤트만 기록합니다. 비즈니스 로직을 수행하지 않습니다.

우리는 세 개의 테이블로 구성된 이벤트 원장(event ledger)을 도입했습니다:

이제 웹훅의 역할은 단 하나입니다:

이는 시스템을 보호합니다. 웹훅이 실패하더라도 이벤트는 안전하게 보관됩니다.

또한 결제 상태와 예약 상태는 서로 다르다는 것을 배웠습니다. 완료된 결제(captured payment)는 입력값입니다. 확정된 예약(confirmed booking)은 결과입니다. 이 둘을 분리해 두어야 대조(reconciliation)가 가능합니다.

조사 과정에서 버그를 발견했습니다. 데이터베이스에는 이벤트가 존재했습니다. 프로세서는 정상 상태였습니다. 웹훅도 정상 상태였습니다.

하지만 프로세서가 전혀 실행되지 않았습니다. 대기 중인 이벤트를 처리하는 함수를 트리거하는 주체가 아무도 없었습니다.

수집(ingestion)과 처리(processing)를 분리하는 것은 좋은 설계입니다. 하지만 이는 새로운 요구사항을 만듭니다. 바로 무언가가 처리를 트리거해야 한다는 점입니다.

우리는 다음과 같은 여러 작업을 실행하기 위해 스케줄러를 구현했습니다:

재시도 중 발생하는 오류를 방지하기 위해 다음과 같은 로직을 사용합니다:

모든 웹훅이 제시간에 도착해야만 작동하는 시스템은 취약한 시스템입니다. 큐(queue)에서 작업을 비워줄(drain) 주체가 없다면, 작업은 영원히 대기하게 됩니다.

신뢰성이란 장애가 발생했을 때를 대비하여 구축하는 것을 의미합니다.

Source: https://dev.to/abhishekvoid/a-successful-payment-that-never-became-a-booking-building-a-fault-tolerant-payment-pipeline-4ioj