Skip to content
isdnetworks
Go back

결제 통지가 두 번 왔다

결제 완료 통지를 받아 처리하는 부분을 맡았는데 주문이 두 번 확정된 건이 나왔다.

Table of contents

Open Table of contents

같은 통지가 두 번 왔다

통지 로그를 보니 같은 내용이 30초 간격으로 두 번 와 있었다.

10:00:01  결제 완료 통지
10:00:31  결제 완료 통지 (같은 주문)

문서를 보니 응답을 못 받으면 일정 시간 뒤에 다시 보낸다고 적혀 있었다. 우리 처리가 여러 단계를 거치느라 30초를 넘었고 그동안 응답이 없으니 저쪽이 재전송한 것이다. 두 번째 통지도 똑같이 처리해서 주문 확정과 재고 차감과 mail() 발송이 전부 두 번 일어났다.

두 번 와도 한 번만 처리하게 했다

고칠 것이 둘이었다. 응답이 느린 것과 두 번 와도 한 번만 처리해야 하는 것인데 뒤쪽을 먼저 했다. 응답을 빠르게 해도 언젠가 두 번 올 수 있기 때문이다. 앞쪽만 고치면 확률이 줄 뿐이고 뒤쪽을 고치면 두 번 와도 안전하다.

받은 통지를 처리하기 전에 먼저 payment_callback 에 기록하고 tidUNIQUE KEY 를 걸었다.

try {
    기록($tid, $payload);
} catch (중복오류 $e) {
    respond_ok();   // 이미 처리함
    return;
}
처리();
respond_ok();

두 번째 통지는 기록 단계에서 ER_DUP_ENTRY 로 걸린다. 코드가 SELECT 로 확인하는 것이 아니라 DB 가 막는다. 확인과 삽입 사이에 틈이 없다는 것이 요점이다.

CodeIgniter 2.x 의 DB 드라이버는 예외를 안 던지므로 _error_number() 가 1062 인지를 봐야 했다. db_debug 가 켜져 있으면 그 자리에서 show_error 로 화면이 죽으니 이 구간만 껐다.

응답을 먼저 주고 처리를 미뤘다

응답이 느린 것도 고쳤다. 기록하고 바로 응답한 뒤에 실제 처리는 statuswait 로 두고 crontab 배치가 가져가게 했다. 응답이 즉시 나가니 재전송이 안 온다.

여기서 걱정이 하나 생겼다. 응답은 했는데 처리가 실패하면 저쪽은 정상으로 알고 끝내므로 다시 안 보낸다. 처리 실패를 우리가 챙겨야 한다.

통지마다 상태를 두고 실패한 것과 오래 대기 중인 것을 매일 확인하게 했다.

SELECT COUNT(*) FROM payment_callback WHERE status = 'fail';

SELECT COUNT(*) FROM payment_callback
 WHERE status = 'wait' AND created_at < DATE_SUB(NOW(), INTERVAL 10 MINUTE);

wait 가 더 안 보이는 문제였다. fail 은 실패라고 표시되니 눈에 띄지만 wait 는 정상 상태처럼 보인다. created_at 에 시간 조건을 붙여야 처리가 안 돌고 있다는 것이 드러난다.

통지의 출처 검증

또 하나 걸린 것이 있었다. 통지를 누가 보냈는지 안 보고 $_POST 값만 읽고 있었다. 그 주소로 아무나 요청을 보내면 결제가 완료된 것으로 처리된다.

문서를 보니 signature 가 함께 온다. 우리만 아는 값으로 같은 hash('sha256', ...) 를 만들어 비교했다.

$expected = hash('sha256', $tid . $amount . $secret);
if ($expected !== $_POST['signature']) { 거부 }

비교는 hash_equals 로 했다. 한 글자씩 끊어 보는 비교는 걸린 시간으로 어디까지 맞았는지가 새어 나간다.

서명이 맞아도 amount 가 주문 금액과 같은지 따로 봤다. 다르면 서명과 무관하게 뭔가 잘못된 것이다.

통지를 받고 저쪽에 tid 로 다시 조회해 실제 상태를 확인하는 방법도 문서에 있었다. 서명은 빠르고 조회는 느린 대신 확실하다. 금액이 큰 건은 조회까지 하게 했다.

정리


Share this post on:

Previous Post
메뉴를 코드에서 찾고 있었다
Next Post
확장자와 내용이 달랐다