서비스 운영 ·

웹훅이 결제와 알림 사건을 서버에 전달하는 방식

먼저 답부터

웹훅은 결제 완료나 저장소 변경 같은 사건이 생겼을 때 외부 서비스가 내 서버의 약속된 주소로 HTTP 요청을 보내는 방식입니다. 서버는 화면 성공 문구를 믿지 말고 서명, 사건 종류, 사건 ID를 확인하며 먼저 빠르게 응답한 뒤 오래 걸리는 작업은 대기열에서 처리해야 합니다.

웹훅이 결제와 알림 사건을 서버에 전달하는 방식 대표 이미지

결제가 끝났는지 매초 결제사에 물어보거나 저장소가 바뀌었는지 계속 확인하면 요청과 비용이 늘어납니다. 웹훅은 외부 서비스에서 사건이 생겼을 때 내 서버에 먼저 알려 주는 방식입니다. 현관 밖에 서서 손님이 왔는지 계속 보는 대신 초인종 소리를 기다리는 모습과 가깝습니다.

초인종이 울렸다고 문을 바로 열어 주지는 않습니다. 누구인지 확인하고, 같은 사람이 두 번 누른 것인지 보고, 무거운 짐은 통로를 막지 않게 옮겨야 합니다. 웹훅도 받기, 진짜인지 확인하기, 중복 막기, 실제 작업 처리하기를 나눠야 안전합니다.

웹훅은 외부 서비스가 먼저 보내는 HTTP 요청이다

webhook은 특정 사건이 생기면 한 서비스가 다른 서비스의 미리 등록된 주소로 보내는 HTTP 요청입니다. 일반 API 호출에서는 내 프로그램이 자료를 달라고 먼저 묻지만, 웹훅에서는 결제사나 GitHub 같은 제공자가 사건 발생을 먼저 알려 줍니다.

{
  "id": "evt_20260911_001",
  "type": "payment.completed",
  "data": { "orderId": "order_42" }
}

이 예시는 사건 ID, 사건 종류, 관련 주문 번호만 보여 줍니다. 실제 필드와 이름은 제공사마다 다르므로 공식 문서를 확인해야 합니다. 화면에서 “결제 성공”이 보였다는 사실과 결제사 서버가 완료 사건을 보냈다는 사실도 따로 기록합니다.

웹훅 주소는 공개돼도 아무 요청이나 믿지 않는다

endpoint는 요청을 받도록 만든 서버 주소입니다. 웹훅 주소는 외부 서비스가 접근해야 하므로 인터넷에서 열려 있지만, 주소를 안다는 이유만으로 요청이 진짜가 되지는 않습니다. 제공사가 보내는 서명과 내 서버에 저장된 비밀값을 이용해 요청 본문이 중간에 바뀌지 않았는지 검증합니다.

signature는 요청이 약속한 제공자에게서 왔는지 확인하는 서명값입니다. Stripe는 서명 검증에 원본 요청 본문과 서명 헤더, endpoint secret이 필요하다고 안내합니다. 본문을 먼저 JSON으로 바꿔 버리면 검증이 실패할 수 있으므로 현재 프레임워크의 공식 예시를 그대로 확인합니다.

  • 웹훅 비밀값은 서버 비밀 저장소에 둡니다.
  • 주소의 쿼리 문자열에 비밀값을 넣지 않습니다.
  • HTTPS로 암호화된 연결을 사용합니다.
  • 사건 종류와 필요한 필드를 허용 목록으로 확인합니다.
  • 검증 실패는 작업하지 않고 안전한 로그만 남깁니다.

같은 사건이 다시 올 수 있다고 가정한다

외부 서비스는 응답을 받지 못했거나 일시 오류가 나면 같은 사건을 다시 보낼 수 있습니다. 인터넷 연결이 끊긴 순간 제공자는 내 서버가 처리했는지 알 수 없기 때문입니다. 따라서 한 번만 도착한다는 가정으로 주문 상태를 바꾸면 중복 메일이나 중복 적립이 생길 수 있습니다.

GitHub는 전달을 구분하는 고유 헤더를 사용해 각 사건이 이미 처리됐는지 확인하라고 안내합니다. 제공사 사건 ID를 서버에 저장하고, 같은 ID가 다시 오면 앞선 처리 결과를 확인합니다. 중복을 막기 위한 기록은 사용자 화면의 버튼 비활성화와 별도입니다.

빠르게 받았다고 답하고 긴 작업은 뒤로 보낸다

웹훅 요청 안에서 이미지 생성, 대량 메일, 보고서 작성까지 끝내려 하면 제공사의 응답 제한 시간을 넘길 수 있습니다. GitHub 공식 문서는 빠르게 2XX 응답을 보내고 비동기 대기열에서 payload를 처리하는 방식을 권합니다. async는 요청을 붙잡아 두지 않고 뒤에서 이어 처리하는 방식입니다.

1. 원본 요청 받기
2. 서명과 사건 종류 확인
3. 사건 ID를 안전하게 기록
4. 작업 대기열에 넣기
5. 2XX로 접수 응답
6. 작업자가 실제 후속 작업 실행

2XX 응답은 사건을 받았다는 뜻이지 주문 처리까지 모두 끝났다는 뜻으로 쓰지 않습니다. 사용자 화면은 “결제가 확인됐습니다”와 “상품 준비가 끝났습니다”를 별도 상태로 보여 줄 수 있습니다.

사건 순서가 항상 기대대로 오지는 않는다

두 사건이 거의 동시에 생기면 네트워크 상황에 따라 뒤 사건이 먼저 도착할 수 있습니다. 먼저 가입하고 나중에 결제한다고 생각했는데 결제 사건이 가입 갱신보다 앞서 올 수도 있습니다. payload 한 건만 믿기보다 제공사 API에서 현재 자원 상태를 다시 조회해야 하는 경우가 있습니다.

사건 종류별로 허용할 상태 전이를 정합니다. 이미 환불된 주문을 결제 완료로 되돌리지 않고, 오래된 사건이 현재 상태를 덮지 않게 사건 시각과 현재 자료를 함께 봅니다. 단, 제공사 시각 하나만 믿지 말고 문서가 보장하는 순서와 재전송 정책을 확인합니다.

개발용 전달과 운영 전달을 섞지 않는다

테스트 모드 사건과 실제 결제 사건은 키와 주소를 분리합니다. 개발 중에는 제공사의 전달 기록이나 CLI 도구로 예제 사건을 보내되 실제 카드 정보와 고객 자료를 복사하지 않습니다. 운영 endpoint를 로컬 임시 주소로 바꾸거나 비밀값을 채팅에 붙여 넣지 않습니다.

로그에는 사건 ID, 종류, 검증 결과, 처리 상태, 오류 코드 정도만 남깁니다. 카드 번호, 비밀번호, 전체 사용자 본문은 기록하지 않습니다. 실패 사건을 다시 보낼 때도 이미 처리된 후속 작업이 없는지 먼저 조회합니다.

오늘 바로 해볼 작은 실습

실제 결제 설정을 바꾸지 않고 사건 처리표를 만듭니다.

  1. “결제 완료”처럼 받을 사건 하나를 정합니다.
  2. 사건 ID와 종류와 관련 자원 ID 세 칸을 그립니다.
  3. 서명 성공과 실패일 때의 행동을 나눕니다.
  4. 같은 사건 ID가 두 번 왔을 때 돌려줄 결과를 적습니다.
  5. 즉시 할 일과 대기열에서 할 일을 구분합니다.
  6. 접수, 처리 중, 완료, 실패 네 상태의 사용자 문장을 씁니다.

모르는 웹훅 재전송 명령을 운영 계정에서 실행하지 마세요. endpoint secret, API 키, 결제 정보는 코드·브라우저·문서·AI 채팅에 넣지 않습니다. 서명 검증과 중복 사건 처리, 현재 주문 상태 조회를 설명할 수 없으면 실제 결제 웹훅을 켜거나 과거 사건을 일괄 재전송하지 않는 것이 안전합니다.

확인한 자료

함께 읽기

  • 오류와 로그를 진료 기록처럼 읽고 원인을 좁히는 순서

    디버깅은 오류 문장을 지우는 일이 아니라 같은 문제를 다시 만들 수 있는 조건을 기록하고 첫 실패 지점을 찾는 과정입니다. 화면 증상, 입력, 시간, 파일과 줄 번호, 기대 결과를 분리해 적은 뒤 한 번에 한 가지만 바꾸면 로그의 결과와 실제 원인을 혼동할 가능성이 줄어듭니다.

  • 저장과 빌드와 배포를 포장 배송 공개처럼 구분하기

    저장은 작업 파일을 남기는 일, 빌드는 소스 코드를 서비스용 결과물로 만드는 일, 배포는 그 결과물을 서버에 올리는 일입니다. 배포 성공 표시만으로 실제 공개 주소가 새 결과물을 제공한다고 단정할 수 없으므로 도메인, 번들, 핵심 기능과 메타데이터를 공개 환경에서 다시 확인해야 합니다.

  • GitHub Actions로 커밋마다 테스트가 도는 흐름 이해하기

    GitHub Actions의 CI는 push나 pull request 같은 사건이 생기면 별도 실행 컴퓨터인 러너에서 설치와 테스트 단계를 자동 수행하는 방식입니다. 워크플로 파일의 실행 조건, 권한, 비밀값 사용, 통과한 정확한 커밋을 확인해야 하며 초록 표시가 실제 배포나 모든 기능의 정상 동작까지 보장하지는 않습니다.