결제 완료 처리가 가끔 안 돼서 결제는 됐는데 orders.status 가 READY 로 남는 일이 있었다. 흐름이 우리 화면에서 PG 화면으로 나갔다가 returnUrl 로 돌아오는 구조라 그 사이에 무슨 일이 있었는지를 알 수 없었다.
Table of contents
Open Table of contents
원인 — 돌아오지 못하는 경우들
완료 처리를 returnUrl 에서만 하고 있었으므로 못 돌아오면 처리가 안 된다. 결제 뒤 브라우저를 닫거나 망이 끊기거나 복귀 요청이 실패하거나 우리 서버가 잠깐 죽어 있는 네 경우가 있었다.
전부 결제는 됐는데 우리만 모르는 상태가 된다. 한 경로에만 의존하는 완료 처리는 그 경로가 실패하는 만큼 그대로 누락이 된다.
조치 — 서버 경로를 함께 받기
PG 가 서버로 직접 알려 주는 noti 웹훅 경로가 있어서 그것도 받게 했다. 브라우저가 안 돌아와도 그 경로로 들어온다.
둘 다 받되 어느 쪽이 먼저 와도 되게 하고 이미 PAID 면 그냥 반환하게 했다. lockForUpdate() 를 걸고 상태를 확인하므로 두 번 와도 한 번만 처리된다.
주의 — 마지막 안전망
서버 통보도 실패할 수 있으므로 우리가 물어보는 경로를 하나 더 뒀다. 주기는 두 가지 방법이 있었는데 1분마다 전수 조회하는 것과 30분 지난 READY 만 모아 조회하는 것이다. 뒤쪽을 골랐다. 이유는 대부분 브라우저 복귀나 웹훅으로 이미 끝나 있어 전수 조회는 PG 호출 한도만 소모해서다. 대신 두 경로가 다 실패한 주문은 최대 30분 늦게 처리되는 것을 감수했다. 남은 주문을 모아 PG 의 조회 API 에 직접 물어본다.
결제된 것으로 나오면 완료 처리하고 통보 누락으로 로그를 남긴다. 이렇게 브라우저 복귀와 서버 통보와 사후 조회 셋 중 하나만 되면 처리되는 구조가 됐다.
검증 — 통보의 진위 확인
서버 통보는 주소만 알면 누구나 보낼 수 있으므로 HMAC 서명을 확인했다. 확인하지 않으면 아무나 완료 처리를 시킬 수 있다.
금액도 주문 금액과 대조했는데 서명이 맞더라도 금액이 다르면 거절한다. 밖에서 들어오는 통보는 인증과 값 검증을 둘 다 거쳐야 근거가 된다.
배경 — 화면과 대조와 실패 기록
브라우저가 돌아왔는데 아직 처리가 안 된 경우에는 확인 중이라는 화면을 보이고 뒤에서 상태를 물었다. 일정 시간 안에 안 되면 확인 중 화면으로 보내서 실패로 보이지 않게 했다.
우리 쪽이 READY 인데 PG 에는 결제된 건이 있는지를 매일 아침 payment:reconcile 로 대조했고 처음 돌렸을 때 지난 것 중 열두 건이 나왔다. 실패한 결제도 fail_reason 과 원본 응답을 남기게 했더니 사유별 분포가 보였고 가장 많은 것에 맞춰 화면 안내를 고쳤다.
정리
- 흐름이 우리 밖으로 나가면 돌아오는 길을 설계해야 한다
- 브라우저가 안 돌아올 수 있으므로 서버 경로 통보도 받는다
- 둘 다 안 와도 우리가 물어보는 마지막 안전망을 둔다
- 어느 쪽이 먼저 와도 되게 하고 두 번 와도 한 번만 처리한다
- 통보는 서명과 금액을 둘 다 확인한다
- 아직 처리 전이면 기다리는 화면을 보인다
- 매일 대조해서 0이 아니면 본다
- 실패한 결제도 사유를 남겨야 안내를 고칠 수 있다