lifecarelog
백엔드

결제 webhook 중복 처리, HMAC 서명검증과 idempotency 키로 막기

결제 webhook은 재시도 때문에 같은 이벤트가 두 번 올 수 있어요. HMAC 서명검증과 DB UNIQUE 제약 기반 idempotency 키로 중복 처리를 막는 구현을 정리했어요.

5분 읽기

결제 webhook 핸들러를 만들면서 "같은 이벤트가 두 번 오면 어떻게 되지?"를 생각해 본 적 있나요. 실제로 두 번 와요. 그리고 대비가 없으면 포인트가 두 번 지급되거나, 주문이 두 건 생겨요.

왜 같은 webhook이 다시 올까요

결제사 대부분은 전달을 보장하려고 at-least-once 방식으로 이벤트를 보내요. 우리 서버가 응답을 늦게 주거나 타임아웃이 나면, 결제사는 "못 받았구나" 하고 같은 이벤트를 재전송해요.

이건 버그가 아니라 정상 동작이에요. 그래서 중복을 걸러내는 책임은 받는 쪽, 그러니까 우리 서버에 있어요.

HMAC 서명검증이벤트 ID INSERT(UNIQUE)비즈니스 처리200 응답

1단계: HMAC 서명검증부터

endpoint 주소만 알면 누구나 가짜 결제 이벤트를 보낼 수 있어요. 그래서 결제사가 시크릿으로 만든 서명을 요청마다 검증해야 해요.

직접 구현해 보니 가장 흔한 함정은 파싱된 JSON으로 서명을 계산하는 거였어요. 프레임워크가 body를 먼저 파싱하면 키 순서나 공백이 달라져서 검증이 계속 어긋나요. 서명은 반드시 raw body 기준으로 계산해야 해요.

타임스탬프 검증도 같이 넣는 게 좋아요. 저는 5분 넘게 지난 요청은 버리도록 잡았어요. 오래된 요청을 다시 쏘는 replay 공격을 막아줘요.

2단계: idempotency 키는 DB 제약으로

중복 차단의 핵심은 테이블 하나예요. 이벤트 ID, 처리 상태, 처리 시각. 컬럼 3개면 되고, 이벤트 ID에 UNIQUE 제약을 걸어요.

핸들러는 먼저 이벤트 ID를 INSERT해요. 충돌이 나면 이미 처리한 이벤트니까 아무것도 하지 않고 바로 200을 돌려줘요. 메모리에 "본 이벤트 목록"을 들고 있는 방식은 서버가 두 대만 돼도 깨지니, DB 제약이 답이에요.

한 가지 더요. 키 저장과 비즈니스 처리는 같은 트랜잭션으로 묶어야 해요. 키만 저장되고 처리가 실패하면, 재시도가 와도 걸러져서 결제는 됐는데 처리는 안 된 상태로 남거든요.

3단계: 응답은 빠르게

무거운 작업을 핸들러 안에서 다 하면 타임아웃이 나고, 타임아웃은 재시도를 부르고, 재시도는 다시 중복 시도가 돼요. 검증과 저장을 마치면 빠르게 200을 주고, 오래 걸리는 후속 작업은 분리하는 게 안전해요.

테스트해 보면 이 구조는 검증도 간단해요. 같은 payload를 연달아 두 번 쏘고, DB에 처리 기록이 한 건만 남는지 확인하면 끝이에요. 최소 구현 기준으로 핸들러가 30줄 정도라서 부담도 크지 않았어요.

플랜비(plan-b)에서 동호회 회비 흐름을 만들면서도 이 3단계 구조를 먼저 잡아 봤는데, 돌려 보니 재시도가 와도 기록이 흔들리지 않아서 마음이 편했어요.

결제는 한 번, 처리도 한 번. webhook을 받을 일이 생기면 서명검증과 UNIQUE 제약, 이 두 가지부터 챙겨 보세요.

#webhook#결제연동#idempotency

라이프케어로그 서비스가 궁금하신가요?

AI 기반 건강·일정·재활 관리 앱을 직접 써보세요.

서비스 살펴보기

관련 글

댓글

아직 댓글이 없어요. 첫 댓글을 남겨주세요.