웹훅 설계 실전: 서명 검증과 멱등성으로 남의 호출을 안전하게 받기

🤖 AI Summary
API를 만들 때는 우리가 부르는 쪽이지만, 웹훅은 남의 서버가 우리 엔드포인트를 부르는 구조입니다. 통제권이 반대편에 있다 보니 네 가지가 따라옵니다. 응답이 늦으면 상대가 재시도하고, 같은 이벤트가 두 번 이상 도착하며, 순서도 보장되지 않죠. 게다가 엔드포인트는 인터넷에 열려 있어 위조 요청이 섞일 수 있습니다. 그래서 수신 측 설계는 세 겹이 됩니다. 첫째, 서명을 검증한 다음 복잡한 로직 전에 2xx를 반환하고 실제 처리는 비동기로 넘깁니다. 둘째, 이벤트 ID를 수신 시점에 선점하고 처리를 마친 뒤 확정해 중복 실행을 막습니다. 순서는 보장되지 않으니 필요한 객체를 API로 직접 조회해 메우고요. 셋째, 신뢰하기 전에 IP 허용목록과 서명 검증을 함께 두고, 서명 비교는 반드시 상수 시간 함수로 합니다. 세 겹을 다 갖춰도 처리 단계에서 새는 이벤트는 남으므로 전달 이력 확인과 재전송까지가 한 세트입니다. 사실은 Stripe와 GitHub 공식 문서를 기준으로 하고, 구현 배치는 스피디 판단입니다.
블로그 목차
웹훅은 통제권이 반대편에 있습니다
우리가 외부 API를 호출할 때는 언제 부를지, 얼마나 기다릴지, 몇 번 재시도할지를 우리가 정합니다. 웹훅은 정반대죠. Stripe 공식 문서의 설명대로, 계정에서 이벤트가 발생하면 발신 측이 우리 엔드포인트로 실시간 이벤트 데이터를 전송합니다. 고객의 은행이 결제를 확정했을 때, 분쟁이 제기됐을 때, 정기 결제가 성공했을 때처럼 비동기로 벌어지는 일에 반응하기 위한 구조입니다.
편리한 만큼 전제가 달라집니다. 호출 시점도, 재시도 정책도 상대가 정하고, 전달 순서는 아무도 보장해 주지 않아요. 우리가 할 수 있는 건 어떤 상황이 와도 안전하게 받아 내는 것뿐입니다. 그 상황이 네 가지입니다. 늦은 응답, 중복 도착, 뒤바뀐 순서, 위조 요청. 대응은 세 겹으로 묶입니다. 여기에 코드에 닿기도 전에 전달이 막히는 경우와, 그래도 놓친 이벤트를 되찾는 방법까지 마지막에 덧붙이겠습니다.
첫째, 서명 검증 먼저 그다음 빠른 2xx 응답
가장 먼저 바로잡을 지점은 웹훅을 받자마자 본 작업을 다 처리하고 응답하는 구조입니다. Stripe 공식 문서의 권고는 반대예요. "타임아웃을 유발할 수 있는 복잡한 로직에 앞서 빠르게 성공 상태 코드(2xx)를 반환한다." 문서가 든 예시가 구체적입니다. "회계 시스템에서 고객의 인보이스를 결제 완료로 갱신하기 전에 200 응답을 먼저 반환해야 한다."
왜 이렇게까지 할까요. 응답이 늦으면 발신 측은 그 전달을 실패로 간주하기 때문입니다. Stripe는 라이브 모드에서 "최대 3일 동안 지수 백오프로 이벤트 전달을 시도"합니다. 문서의 전달 오류 표에도 "대상 서버가 웹훅 요청에 응답하는 데 너무 오래 걸렸다"는 항목이 있고, 그 해결책으로 복잡한 로직을 미루고 즉시 성공 응답을 반환하라고 안내합니다. 한 번의 응답 지연이 며칠에 걸친 반복 도착으로 이어질 수 있는 셈이죠. 샌드박스에서 만든 전달은 다릅니다. 몇 시간에 걸쳐 세 번만 재시도하니, 샌드박스에서 관찰한 것을 라이브 전제로 삼으면 안 되고요. 응답을 빨리 주는 것은 예의가 아니라 중복을 스스로 만들지 않기 위한 방어입니다.
짚고 넘어갈 오해가 하나 있습니다. 여기서 미루라는 것은 비즈니스 로직이지 서명 검증이 아닙니다. GitHub 공식 문서는 "전달을 더 처리하기 전에 웹훅 서명을 검증하는 것이 좋다"고 안내하며, 그렇게 하면 "GitHub에서 오지 않은 전달을 처리하는 데 서버 시간을 쓰지 않도록 돕고, 중간자 공격을 피하는 데도 도움이 된다"고 설명하죠. 검증은 오히려 서버 자원을 아끼는 단계라는 뜻입니다.
정리하면 실무 순서는 이렇습니다. 서명을 먼저 검증하고, 이미 처리한 이벤트인지 확인한 뒤, 큐에 적재하고 곧바로 200을 반환합니다. 실제 비즈니스 로직은 큐를 소비하는 쪽에서 비동기로 처리합니다. 여기서 하는 조회는 이벤트 ID 키 조회 한 번까지예요. 이 자리가 무거워지기 시작하면 2xx를 앞당긴 의미가 사라집니다.
단서가 하나 붙습니다. 200은 적재에 성공했을 때만 반환해야 합니다. 큐 쓰기가 실패했는데도 200을 돌려주면 발신 측은 전달에 성공한 것으로 보고 재시도하지 않거든요. 이때는 2xx가 아닌 코드를 돌려주면 됩니다. Stripe 문서도 엔드포인트가 2xx가 아닌 상태 코드로 응답한 경우를 재시도가 일어나는 상황으로 설명하니까요. 서버 측 실패이니 5xx가 자연스럽고, 그 응답이 곧 재전송 요청이 됩니다. 참고로 발신 측이 재시도를 어떻게 설계하는지는 타임아웃·재시도·서킷 브레이커 글에서 다뤘습니다. 이 글은 그 재시도를 받는 쪽의 이야기예요.

둘째, 이벤트 ID 선점과 확정으로 만드는 멱등성
빠르게 응답해도 중복이 완전히 사라지지는 않습니다. Stripe 공식 문서도 이를 전제로 말하죠. "웹훅 엔드포인트는 때때로 같은 이벤트를 두 번 이상 받을 수 있다." 그 전에 부하부터 줄이는 설정도 하나 있습니다. 문서는 연동에 필요한 이벤트 타입만 받도록 엔드포인트를 구성하라고 하며, 여분의 이벤트나 전체 이벤트를 듣는 것은 서버에 과도한 부담을 준다고 권하지 않거든요.
문서가 내놓는 방어책이 이벤트 ID 기록입니다. "처리한 이벤트 ID를 기록해 두고, 이미 기록된 이벤트는 처리하지 않는 방식으로 중복 수신을 막을 수 있다." 멱등하게 만든다는 것은 같은 이벤트가 몇 번 들어와도 결과가 달라지지 않게 한다는 뜻입니다. 여기서 문서가 기록 대상을 "처리한" 이벤트 ID로 명시한 점이 중요하죠. 이벤트 ID를 기록해 두더라도 기록 확정은 처리를 마친 시점이어야 합니다. 수신하자마자 처리한 것으로 적어 두면, 실제 처리가 중간에 실패했을 때 뒤이은 재시도분이 중복으로 걸러져 이벤트가 조용히 사라집니다. 이 구현 순서까지 문서가 규정하지는 않지만, 문서가 붙인 그 조건을 지키려면 필요한 배치입니다.
기록 시점을 뒤로 미루는 것만으로는 끝나지 않습니다. 처리가 진행되는 동안에도 같은 이벤트의 재시도분은 계속 미처리로 보이거든요. 그래서 실무에서는 두 단계로 나눕니다. 수신 시점에 이벤트 ID로 자리를 선점해 중복 선점은 실패하게 만들고, 처리를 마친 뒤에 완료로 확정하는 것이죠. 선점만 있고 완료 확정이 없으면 유실이, 완료 확정만 있고 선점이 없으면 이중 실행이 남고요. 두 단계를 다 둬도 한 가지는 남습니다. 선점만 된 채 처리가 멈춘 건은 뒤이은 재시도분까지 중복으로 걸러져 그대로 묶이거든요. 오래된 선점은 풀어 주거나, 뒤에서 볼 재전송으로 되살려야 합니다. 결제 승인이나 포인트 적립처럼 두 번 실행되면 곤란한 작업일수록 이 두 단계에서 갈립니다.
이벤트 ID만으로 충분하지 않은 경우도 문서에 나옵니다. "어떤 경우에는 두 개의 별도 Event 객체가 생성되어 전송된다"는 것인데, 이때는 이벤트 ID 대조만으로 걸러지지 않죠. 문서가 제시하는 식별 방법은 data.object의 객체 ID를 event.type과 함께 쓰는 것입니다.
순서도 마찬가지죠. Stripe는 "이벤트가 생성된 순서대로 전달되는 것을 보장하지 않는다"고 명시하고, 수신 측이 특정 순서에 의존하지 않도록 하라고 권고합니다.
Stripe가 드는 사례는 구독 생성이죠. 구독 하나를 만들면 customer.subscription.created, invoice.created, invoice.paid가 발생할 수 있고 청구가 있다면 charge.created까지 더해지는데, 이 이벤트들이 생성 순서대로 도착한다는 보장이 없습니다. '이전 이벤트가 왔을 테니 이 상태겠지'라는 가정이 여기서 깨집니다. 문서가 제시하는 대응은 API로 빠진 객체를 직접 조회하는 거예요. invoice.paid를 먼저 받았다면 그 정보로 invoice·charge·subscription 객체를 가져오면 된다고 안내합니다.
셋째, 서명 검증: 상수 시간 비교부터 전달 실패 구간까지
서명 검증 이야기는 세 갈래로 갈립니다. 어떻게 검증하는가, 검증을 통과해도 남는 위험은 무엇인가, 그리고 정상 요청인데 검증이 실패하는 이유는 무엇인가. 순서대로 보겠습니다.
웹훅 엔드포인트는 인터넷에 열려 있습니다. 주소만 알면 누구나 요청을 보낼 수 있죠. Stripe 공식 문서의 경고가 직설적입니다. 검증이 없으면 "공격자가 가짜 웹훅 이벤트를 보내 주문 이행, 계정 접근 권한 부여, 레코드 수정 같은 동작을 유발할 수 있다."
다만 Stripe가 제시하는 방어는 서명 검증 하나가 아닙니다. 문서는 "이 두 가지 보호를 모두 사용하라"며 IP 허용목록을 함께 들죠. Stripe는 공개된 IP 목록에서 웹훅을 전송합니다. 서버나 방화벽이 그 주소에서 온 요청만 받도록 설정하라는 뜻이죠. 문서에는 없는 항목이지만, 목록을 방화벽에 넣기 전에 우리 쪽 구성부터 확인해야 합니다. 엔드포인트 앞단에 프록시나 로드밸런서가 있으면 서버가 보는 출발지 주소는 Stripe가 아니라 그 앞단 장비가 되거든요.
두 번째 보호 장치가 서명 검증입니다. Stripe 문서는 모든 웹훅 이벤트에 서명을 넣는다고 밝히고, GitHub는 시크릿을 설정해 둔 경우에 서명 헤더를 함께 보냅니다. Stripe는 Stripe-Signature 헤더에 타임스탬프와 서명을 담고, GitHub는 X-Hub-Signature-256 헤더를 씁니다.
GitHub 문서는 서명 생성 방식을 이렇게 설명합니다. "GitHub는 여러분의 시크릿 토큰을 사용해 해시 서명을 만들고, 그 서명을 각 페이로드와 함께 보냅니다." GitHub는 해시를 HMAC hex digest로 계산하며, 이 서명은 항상 sha256=으로 시작하죠. 수신 측은 같은 시크릿으로 페이로드의 해시를 계산해 비교하면 되죠. 다만 Stripe 문서는 검증 방법을 두 가지로 두고, 그중 공식 라이브러리 쪽을 권장으로 표시합니다. 페이로드와 Stripe-Signature 헤더, 엔드포인트 시크릿을 넘기면 검증이 끝나고 실패하면 오류가 나는 방식이죠.
직접 구현하더라도 배포 전에 맞춰 볼 값은 있습니다. GitHub 문서가 테스트용 시크릿과 페이로드, 기대 서명값을 공개해 두거든요. 시크릿 It's a Secret to Everybody, 페이로드 Hello, World!로 계산한 서명이 757107ea0eb2509f…로 시작하는 값과 같으면 구현이 맞는 것입니다. 배포 전에 이 값 하나만 맞춰 봐도 검증 로직의 기본 실수는 걸러집니다.
놓치기 쉬운 지점이 하나 있죠. 바로 비교 방법입니다. GitHub 문서는 단호합니다. "일반 == 연산자를 절대 사용하지 마세요." 대신 secure_compare나 crypto.timingSafeEqual처럼 "상수 시간" 문자열 비교를 수행하는 방법을 쓰라고 안내합니다. 문서는 상수 시간 비교가 "특정 타이밍 공격을 완화하는 데 도움이 된다"고 표현하고, JIT 최적화 언어의 일반 루프도 같은 위험군으로 지목합니다. 비교에 걸리는 시간이 입력에 따라 달라지지 않게 만드는 것이 핵심이죠.
비교에 성공했을 때만큼 중요한 것이 실패했을 때 무엇을 돌려주느냐입니다. Stripe 공식 예제는 서명 검증 오류에서 400을 반환하고, GitHub 예제는 언어별로 500·403·401로 갈립니다. 공통점은 2xx를 주지 않는다는 것이죠.
여기까지가 첫 갈래, 검증 자체입니다. 두 번째 갈래는 검증을 통과한 요청에도 남아 있는 위험이죠. 서명이 유효하다는 것이 요청이 방금 도착했다는 뜻은 아닙니다. Stripe 문서는 재전송 공격을 "공격자가 유효한 페이로드와 그 서명을 가로챈 뒤 다시 전송하는 것"으로 정의하고, 이를 막기 위해 Stripe-Signature 헤더에 타임스탬프를 포함한다고 설명합니다. 타임스탬프도 서명 대상에 포함되므로, 시각을 건드리면 서명이 곧바로 무효가 되죠.
그래서 서명이 유효해도 타임스탬프가 너무 오래됐다면 페이로드를 거부할 수 있습니다. 공식 라이브러리의 기본 허용 오차는 5분이고, 문서는 허용 오차 값으로 0을 쓰지 말라고 명시합니다. 0으로 두면 최신성 검사 자체가 꺼지거든요. 서버 시각을 NTP로 맞춰 두라는 안내도 함께 나옵니다.
한 가지 헷갈리기 쉬운 부분이 있습니다. 5분 허용 오차를 두면 3일에 걸친 재시도가 전부 거부되는 것 아니냐는 의문이 생기죠. 그렇지 않습니다. Stripe는 이벤트를 보낼 때마다 타임스탬프와 서명을 새로 생성하며, 재시도할 때도 그 전달에 대해 새 서명과 새 타임스탬프를 만들어 전송합니다. 허용 오차가 걸러 내는 대상은 재시도가 아니라 가로챈 뒤 나중에 재전송하는 요청입니다.
시크릿 자체의 관리는 GitHub 문서가 짚습니다. 시크릿 토큰은 "엔트로피가 높은 무작위 문자열"로 만들고, "토큰을 애플리케이션에 하드코딩하거나 어떤 저장소에도 푸시하지 말라"고 안내하죠. 이 가운데 하드코딩과 저장소 유출을 막자는 대목은 시크릿 관리 글에서 다룬 이야기와 같습니다. Stripe 쪽은 엔드포인트 서명 시크릿을 주기적으로 교체하라는 권고를 추가로 두고요. 교체할 때 기존 시크릿을 최대 24시간까지 유예할 수 있는데, 그동안은 엔드포인트에 여러 시크릿이 동시에 살아 있고 시크릿마다 서명이 하나씩 생성됩니다. 문서가 결과까지 적어 두지는 않았지만, 수신 측이 서명 하나만 검증하도록 짜여 있다면 이 기간에 검증이 실패할 수 있습니다.
세 번째 갈래는 방향이 반대입니다. 위조가 아니라 정상 요청인데 검증이 실패하거나, 검증에 닿지도 못하는 경우죠. 그중 하나가 원본 본문입니다. Stripe 문서는 서명 검증을 수행하려면 요청의 원본 본문(raw body)이 필요하다고, 경고 표시까지 달아 안내합니다. 프레임워크가 본문을 가공하지 않도록 확인하라는 것인데, 요청 원본 본문에 어떤 조작이 가해져도 검증은 실패하거든요.
분명히 GitHub가 보낸 페이로드인데도 검증이 실패한다면 어디부터 볼지 GitHub 문서가 점검 항목을 나열해 뒀습니다. 시크릿 설정 여부, 헤더 종류, 알고리즘, 시크릿 값, 프록시·로드밸런서의 페이로드·헤더 변조, 인코딩입니다. 목록 맨 앞에 놓인 항목이 시크릿 설정 여부인데, 여기에는 시크릿을 설정하지 않으면 X-Hub-Signature-256 헤더 자체가 오지 않는다는 설명이 붙어 있습니다. 헤더는 특히 헷갈리는 지점입니다. HMAC-SHA1을 쓰는 X-Hub-Signature는 레거시 목적으로만 남아 있고, 권장 헤더는 X-Hub-Signature-256이죠. 인코딩을 지정하는 언어라면 페이로드를 UTF-8로 처리하라는 안내도 있습니다. 유니코드 문자가 들어올 수 있으니, 한국어 페이로드를 받는다면 그냥 넘길 항목이 아니고요.
코드에 닿기도 전에 전달이 실패하는 구간도 있습니다. Stripe 문서의 전달 오류 표는 3xx 응답에 대해 리다이렉트 응답을 전달 실패로 간주한다고 명시하고, 리다이렉트가 최종적으로 도착하는 URL을 엔드포인트 주소로 지정하라는 해결책을 답니다. http에서 https로, www에서 non-www로 넘기는 설정이 걸려 있으면 코드가 완벽해도 이벤트가 한 건도 도달하지 않을 수 있죠. 전송 구간에도 조건이 붙고요. 라이브 모드에서는 HTTPS가 필수이고, Stripe 웹훅은 TLS 1.2와 1.3만 지원합니다.
검증 로직과 별개로 프레임워크 설정 때문에 막히는 경우도 있습니다. Stripe 문서는 Rails나 Django를 쓰면 사이트가 모든 POST 요청에 CSRF 토큰이 있는지 자동으로 검사할 수 있다며, 그런 경우 웹훅 라우트를 CSRF 보호에서 예외 처리해야 할 수도 있다고 안내합니다.
놓친 웹훅 이벤트를 되찾는 법: 재전송과 관측
여기까지 따라오면 한 가지가 남습니다. 적재에 성공해 2xx를 돌려준 뒤, 정작 처리하는 쪽이 실패하면 어떻게 되느냐는 것이죠. 발신 측은 이미 전달에 성공했다고 보므로 자동 재시도는 오지 않습니다. 빠른 응답을 택한 대가입니다.
그래서 이 구조에는 두 가지가 따라붙어야 하죠. 먼저 관측입니다. Stripe는 Workbench의 Event deliveries 탭에서 이벤트별로 Delivered·Pending·Failed 상태를 보여 주고, 이벤트를 클릭하면 전달 시도의 HTTP 상태 코드와 다음 재시도 예정 시각까지 확인할 수 있다고 안내합니다. 우리 쪽 로그만 보고 있으면 전달 자체가 실패한 건인지 처리가 실패한 건인지 구분되지 않죠.
다음은 복구입니다. 문서는 수동 재시도 방법을 두 가지로 제시합니다. 대시보드에서 해당 이벤트의 Resend를 누르는 방법은 이벤트 생성 후 15일까지, Stripe CLI로 stripe events resend [event_id] --webhook-endpoint=[endpoint_id]를 실행하는 방법은 30일까지 동작합니다. 함정도 같이 적혀 있어요. 전달 실패 이력이 있는 이벤트를 수동으로 재전송해도 2xx가 나오든 말든 Stripe의 자동 재시도 동작이 사라지지는 않는다는 것입니다. 즉 수동 재전송이 곧 중복 수신이 될 수 있으니, 앞서 만든 멱등 장치가 여기서 다시 일합니다.
이 숫자들은 이벤트 ID 기록을 얼마나 오래 들고 있어야 하는지도 알려 주죠. 자동 재시도만 라이브 모드 기준 3일이고 수동 재전송은 30일까지 가능하니, 멱등 레코드를 하루 이틀 만에 지우면 뒤늦게 도착한 재전송분이 그대로 한 번 더 실행됩니다.

여기 정리한 내용은 Stripe와 GitHub가 문서화한 방식이 기준입니다. 서명 헤더도, 재시도 정책도 발신 측마다 다르니 연동하려는 서비스의 웹훅 문서를 먼저 펼쳐 보세요.
그래도 관점은 같습니다. 웹훅 수신은 상대를 믿지 않는 설계예요. 응답은 빨리 주되 처리는 미루고, 같은 이벤트가 또 와도 무해하게 만들고, 서명을 확인하기 전까지는 어떤 요청도 신뢰하지 않는 것. 그리고 세 겹을 다 갖춰도 처리 단계에서 조용히 새는 이벤트는 남으니, 전달 이력을 보고 되찾는 경로까지 있어야 비로소 상대가 언제 몇 번을 보내든 흔들리지 않습니다.
이것만 기억하세요
웹훅은 상대가 언제 몇 번 보낼지 우리가 정할 수 없는 구조입니다. 그래서 받는 쪽에서 지킬 것이 분명해집니다. 서명을 먼저 확인할 것, 서명 비교는 == 대신 상수 시간 비교 함수를 쓸 것, 적재에 성공했을 때만 2xx를 돌려줄 것, 이벤트 ID는 수신 시점에 선점하고 처리를 마친 뒤 확정할 것. 그리고 이 구조에서는 처리 실패분을 발신 측 자동 재시도가 메워 주지 않으니, 전달 이력을 보고 수동 재전송으로 복구하는 경로까지 준비해 둡니다.
자주 묻는 질문 (FAQ)
Q. 웹훅을 받으면 왜 복잡한 로직 전에 2xx를 반환해야 하나요?
발신 측이 응답 코드로 전달 성공 여부를 판단하기 때문이에요. Stripe 공식 문서는 타임아웃을 유발할 수 있는 복잡한 로직에 앞서 빠르게 2xx를 반환하라고 안내합니다. 응답이 늦으면 그 전달은 실패로 기록되고 재시도가 이어지죠. 실제 처리는 큐에 적재해 비동기로 진행하는 편이 안전합니다. 다만 2xx보다 앞에 두어야 할 단계가 하나 있어요. 서명 검증입니다. GitHub 공식 문서는 전달을 더 처리하기 전에 서명을 검증하라고 안내합니다.
Q. 같은 웹훅 이벤트가 두 번 올 수도 있나요?
그렇습니다. Stripe 공식 문서는 웹훅 엔드포인트가 때때로 같은 이벤트를 두 번 이상 받을 수 있다고 명시해요. 방어책은 이벤트 ID를 기록해 두는 것인데, 기록을 두 단계로 나누는 게 중요해요. 문서가 규정한 건 "처리한" 이벤트 ID를 기록하라는 데까지고, 두 단계로 나누는 것은 그 조건을 지키기 위한 구현 배치예요. 수신 시점에 ID로 자리를 선점해 중복 선점은 실패하게 만들고, 처리를 마친 뒤에 완료로 확정합니다. 선점만 있으면 처리가 실패했을 때 이벤트가 유실되고, 완료 확정만 있으면 처리 중에 도착한 재시도분이 그대로 한 번 더 실행되거든요. 두 단계를 다 둬도 선점만 된 채 멈춘 건은 남으니, 오래된 선점을 풀어 주는 장치나 수동 재전송 복구 경로가 함께 있어야 합니다.
Q. 이벤트가 순서대로 오지 않으면 어떻게 하나요?
순서에 의존하지 않도록 설계해야 합니다. Stripe 공식 문서는 이벤트가 생성된 순서대로 전달되는 것을 보장하지 않는다고 밝히며, 특정 순서에 의존하지 말라고 권고해요. 문서가 제시하는 대응은 API로 빠진 객체를 직접 조회하는 것입니다.
Q. 웹훅 이벤트가 유실된 것 같으면 어떻게 하나요?
먼저 어디서 끊겼는지부터 봐야 해요. Stripe는 Workbench의 Event deliveries 탭에서 이벤트별 Delivered·Pending·Failed 상태와 전달 시도의 HTTP 상태 코드, 다음 재시도 예정 시각을 보여 줍니다. 전달 자체가 실패했다면 대시보드에서 Resend를 누르는 방법이 이벤트 생성 후 15일까지, Stripe CLI의 재전송 명령이 30일까지 동작합니다. 다만 전달 실패 이력이 있는 이벤트를 수동으로 재전송해도 자동 재시도 동작은 사라지지 않으니, 중복 수신을 전제로 멱등 장치를 갖춘 뒤에 사용해야 합니다.
Q. 서명을 비교할 때 == 를 쓰면 안 되나요?
안 됩니다. GitHub 공식 문서는 일반 == 연산자를 절대 사용하지 말라고 명시하며, secure_compare나 crypto.timingSafeEqual처럼 상수 시간 문자열 비교를 수행하는 방법을 쓰라고 안내해요. 문서는 이 방식이 특정 타이밍 공격을 완화하는 데 도움이 된다고 표현하고, JIT 최적화 언어의 일반 루프도 같은 위험군으로 지목합니다.



