응답이 없는 것과 실패한 것은 다릅니다

결제 연동 11년의 기본값, 타임아웃·멱등·폴백

결제 코드에는 다른 코드에 없는 긴장이 있습니다. 내 시스템 바깥, 남의 서버에서 돈이 움직이니까요. 오픈갤러리에서 11년 동안 결제를 맡으면서 제일 자주 들여다본 것은 응답이 오지 않은 건들이었습니다. 그 시간이 남긴 기본값이 세 가지 있습니다. 타임아웃, 멱등, 폴백. 소개 페이지에 한 줄로 적어 둔 말을 풀어 적습니다.

미확정이라는 상태

결제 요청을 보냈는데 응답이 오지 않습니다. 실패했을까요? 모릅니다. 요청이 도착하기 전에 끊겼다면 돈은 움직이지 않았고, 응답이 돌아오다 끊겼다면 돈은 이미 움직였습니다. 같은 무응답이 정반대의 두 현실을 담고 있습니다.

타임아웃은 몇 초를 기다릴지 정하는 장치처럼 보이지만, 실제로 정하는 것은 모른다는 상태를 시스템에 어떻게 새길지입니다. 저는 무응답을 실패로 처리하지 않고 미확정으로 남겨 둡니다. 결제사에 그 거래가 실제로 있었는지 조회해 확정되기 전까지, 그 주문은 어느 쪽으로도 진행되지 않게 묶어 둡니다. 급한 것은 판정이 아니라 보류입니다.

두 번 오는 요청

외부 연동에서는 같은 요청이 늘 다시 옵니다. 응답을 못 받은 상대는 다시 보내고, 결제창 앞에서 기다리다 지친 사용자는 다시 누릅니다. 웹훅은 애초에 될 때까지 다시 보낸다는 약속으로 설계된 채널입니다.

그래서 받는 쪽의 기본값은 멱등입니다. 같은 요청이 몇 번 와도 결과는 한 번이어야 합니다. 주문에는 고유한 키를 붙이고, 알림을 받으면 처리하기 전에 먼저 묻습니다. 이미 처리한 알림은 아닌가. 2023년 결제 웹훅을 다시 설계할 때도 이 질문을 기능 목록보다 앞에 두었습니다. 몇 번을 받아도 결과가 한 번으로 수렴하는 것, 그것이 설계의 첫 번째 조건이었습니다.

장애가 머무는 자리

외부 시스템은 언젠가 멈춥니다. 점검으로, 지연으로, 때로는 이유를 모른 채. 그때 결제 한 건이 밀리는 것과 서비스 전체가 멈추는 것은 완전히 다른 사건입니다.

그래서 외부를 부르는 자리마다 경계부터 그었습니다. 기다림에는 상한을 두고, 실패한 알림은 버리지 않고 쌓아 두었다가 순서대로 다시 처리했으며, 외부가 흔들릴 때 실서비스 쪽 요청부터 지키도록 차단 지점을 따로 뒀습니다. 폴백의 목표는 하나입니다. 문제를 문제가 난 자리에 가두는 것.

마지막 1,000원

이 기본값들이 실제로 동작하는지는 테스트 환경이 다 말해 주지 않습니다. 2015년에 가상계좌 입금을 실시간으로 받는 모듈을 만들었을 때, 마지막 확인은 제 돈 1,000원을 실제 가상계좌에 입금해 보는 것이었습니다. 테스트 서버의 약속과 운영의 현실은 다를 수 있으니까요. 결제 연동의 마지막 단계는 언제나 그랬습니다. 문서를 믿되, 돈이 실제로 움직이는 것을 눈으로 확인하기 전까지는 끝났다고 말하지 않기.

결제가 없는 곳에서

도록(dolog)에는 지금 결제가 없습니다. 그래도 바깥에 기대는 자리는 그대로 있습니다. 이미지 저장소를 부르는 자리, 메일을 보내는 자리마다 같은 질문을 합니다. 응답이 없으면 어떻게 되는가. 두 번 오면 어떻게 되는가. 여기가 멈추면 어디까지 멈추는가. 결제에서 배운 의심은 돈이 오가지 않는 곳에서도 제값을 합니다.