Junglog.

Webhook 개념

Webhook 개념 이해

최정환·2025년 3월 30일·6분 읽기193 views

API로 요청했는데, 결과는 왜 웹훅으로 다시 받을까?

웹훅을 설명하려고 보니, 나도 정의부터 꺼내고 있었다.

“이벤트가 발생하면 등록된 URL로 HTTP 요청을 보낸다.”

틀린 설명은 아니다. 그런데 이 문장만으로는 왜 웹훅이 필요한지 잘 와닿지 않는다. 이미 API로 요청을 보내고 응답까지 받았는데, 굳이 다른 요청을 받아야 하는 이유는 무엇일까?

이 질문을 확인하려고 요청을 접수한 뒤 나중에 작업을 끝내는 서버를 만들어봤다. API는 다른 프로그램이 기능을 호출할 수 있도록 제공하는 인터페이스다. 이 글에서는 HTTP 요청으로 호출하는 API를 다룬다.

응답을 받았다고 작업이 끝난 것은 아니다

외부 서비스에 작업을 요청했다고 하자. 응답에는 작업 번호와 함께 ‘처리 중’이라는 상태가 담겨 있다.

외부 서비스는 요청을 정상적으로 접수했다. 하지만 실제 작업은 아직 끝나지 않았다. 원래 응답에 최종 결과를 담으려면 작업이 끝날 때까지 연결을 유지해야 한다. 작업 시간이 길거나 일정하지 않으면 호출하는 쪽에서 응답을 기다리다 시간 초과가 발생할 수 있다.

이럴 때 요청 접수와 결과 전달을 나눌 수 있다. 먼저 작업 번호를 반환하고, 완료 결과는 나중에 전달하는 방식이다. 실제로 Stripe도 은행의 결제 확인처럼 나중에 결과가 정해지는 이벤트를 웹훅의 사용 사례로 설명한다.[1]

음식점에서 주문한 뒤 진동벨을 받는 상황과 비슷하다. 진동벨을 받았다는 것은 주문이 접수됐다는 뜻이다. 음식이 준비됐다는 뜻은 아니다.

그렇다면 우리 서버는 작업이 끝났다는 사실을 어떻게 알 수 있을까?

계속 물어보거나, 끝났을 때 알려주거나

첫 번째 방법은 작업 번호로 상태 조회 API를 반복 호출하는 것이다. ‘처리 중’이면 잠시 기다렸다가 다시 조회하고, ‘완료’가 나오면 다음 작업을 진행한다. 이렇게 주기적으로 확인하는 방식을 폴링(Polling)이라고 한다.

두 번째 방법은 외부 서비스에 알림을 받을 주소를 미리 알려주는 것이다. 작업이 끝나면 외부 서비스가 그 주소로 완료 정보를 보낸다. 우리 서버가 묻기 전에 외부 서비스가 알려주는 방식이다.

이것이 웹훅(Webhook)이다. 특정 이벤트가 발생했을 때, 미리 등록한 URL로 HTTP 요청을 보내 알리는 방식이다. 여기서 이벤트는 ‘작업 완료’처럼 시스템에서 발생한 변화를 뜻한다.

진동벨 비유로 돌아가면 폴링은 음식이 나왔는지 카운터에 계속 물어보는 쪽이고, 웹훅은 음식이 준비됐을 때 벨이 울리는 쪽이다.

차이는 요청 방향에 있다. 최초 작업 요청은 우리 서버가 외부 서비스로 보낸다. 웹훅 요청은 외부 서비스가 우리 서버로 보낸다. 최초 응답이 나중에 바뀌는 것이 아니라, 별도의 HTTP 요청이 새로 오는 것이다.

그림에서 202는 요청을 접수했지만 처리가 끝나지 않았음을 나타내는 HTTP 상태 코드다. 실제 서비스의 접수 응답 형식은 제공 업체마다 다르다.

또한 웹훅이 항상 이전 API 요청의 결과를 전달하는 것은 아니다. 정기 결제처럼 우리 서버가 그때 직접 요청하지 않았더라도, 외부에서 발생한 변화를 알릴 수 있다.

서버 두 개로 직접 확인해봤다

로컬에 외부 서비스 역할의 서버와 웹훅을 받는 서버를 띄웠다. 외부 서비스는 작업을 접수하면 ‘처리 중’을 반환하고, 약 0.3초 뒤 완료 상태로 바꾸도록 만들었다.

같은 조건에서 폴링 방식과 웹훅 방식을 각각 실행했다. 폴링은 첫 조회를 바로 보내고, 미완료 응답을 받으면 0.05초씩 기다린 뒤 다시 조회했다. 웹훅 방식은 수신 주소를 전달하고 완료 알림을 기다렸다.

실행 결과는 다음과 같다.

text
polling initial: HTTP 202, status=PROCESSING
polling result: status=COMPLETED, GET requests=7
webhook initial: HTTP 202, status=PROCESSING
webhook result: status=COMPLETED, GET requests=0, webhook POST requests=1

이번 실행에서 폴링은 완료를 확인하기까지 상태 조회 요청을 7번 보냈다. 웹훅은 상태 조회 요청 없이 완료 알림 1번을 받았다. 최초 작업 요청은 두 방식 모두 1번씩 보냈으며, 위의 조회 횟수에는 포함하지 않았다.

7이라는 숫자는 고정된 결과가 아니다. 작업 시간, 조회 간격, 실행 환경에 따라 달라진다. 이 실험에서 확인하려던 것은 성능 차이가 아니라, 완료를 확인하기 위해 누가 요청을 보내는가였다.

웹훅을 받았다면 완료로 처리해도 될까?

흐름만 보면 간단하다. 완료 알림을 받았으니 우리 쪽 상태도 완료로 바꾸면 될 것 같다.

하지만 ‘외부에서 요청이 왔다’는 사실과 ‘믿을 수 있는 완료 결과를 처리했다’는 사실은 다르다. 그 사이에 확인할 일이 있다.

누가 보낸 요청인지 확인해야 한다

웹훅 수신 주소를 안다고 해서 누구나 주문 상태를 바꿀 수 있어서는 안 된다. 제공 업체가 지원하는 인증 방식을 확인해야 한다.

그중 하나가 HMAC(Hash-based Message Authentication Code), 즉 비밀 키와 메시지로 만든 인증 코드다. 보내는 쪽과 받는 쪽이 공유한 비밀 키를 이용해 서명을 검증하면 메시지의 출처와 변조 여부를 확인할 수 있다.

이때 서명 대상과 검증 방법은 업체의 규약을 따라야 한다. 예를 들어 Stripe는 수신한 원본 요청 본문을 서명 검증에 사용하도록 요구한다.[1] 같은 데이터라도 JSON을 다시 직렬화하면서 공백이나 필드 순서가 바뀌면 원본 바이트와 달라질 수 있다.

같은 완료 알림이 다시 올 수 있다

우리 서버가 완료 처리를 했는데, 응답이 외부 서비스에 도착하지 않았다고 하자. 외부 서비스는 수신 여부를 확인할 수 없으므로 같은 이벤트를 다시 보낼 수 있다. 재시도 조건과 횟수는 업체마다 다르다.

따라서 같은 알림을 다시 받아도 주문 처리나 포인트 지급이 중복 실행되지 않아야 한다. 이렇게 같은 요청을 여러 번 처리해도 결과가 달라지지 않도록 하는 성질을 멱등성(Idempotency)이라고 한다.

이벤트 식별자로 이미 처리한 알림인지 확인하는 것이 출발점이다. 다만 확인과 기록을 분리하면 동시에 도착한 두 요청이 모두 ‘처리 이력 없음’을 확인할 수 있다. 중복 방지는 데이터베이스의 고유 제약이나 트랜잭션처럼 동시 요청까지 제어하는 장치와 함께 설계해야 한다.

또한 이벤트 식별자만 같지 않다고 해서 업무까지 별개인 것은 아니다. 같은 주문에 대한 서로 다른 알림이 동일한 후속 처리를 유발한다면, 주문 기준의 중복 방지도 필요하다.

빨리 응답하되, 알림을 잃어버리면 안 된다

무거운 처리를 모두 끝낸 뒤 응답하면 보내는 쪽에서 시간 초과가 발생할 수 있다. 그래서 수신 처리와 실제 업무 처리를 나누기도 한다.

그렇다고 요청을 받자마자 성공 응답부터 보내면 안전한 것은 아니다. 성공 응답 뒤에 서버가 종료되면, 메모리에만 있던 알림은 사라질 수 있다. 보내는 쪽은 이미 성공으로 판단했으므로 재전송하지 않을 수도 있다.

비동기로 처리한다면, 검증한 알림을 데이터베이스에 커밋하거나 장애 후 복구 가능한 큐에 기록한 뒤 성공 응답을 보내는 구조를 고려해야 한다. 이후 업무 처리에 실패한 알림은 우리 쪽에서 재시도할 수 있어야 한다.

이 구조에서 성공 응답의 의미는 ‘업무 처리를 전부 끝냈다’가 아니라 ‘알림을 맡아서 처리할 수 있게 받았다’가 된다. 이는 저장 후 응답하는 수신 구조를 선택했을 때의 의미다. 실제 응답 조건은 제공 업체의 규약과 우리 처리 방식에 맞춰 정해야 한다.

그러면 폴링은 필요 없어지는 걸까?

웹훅을 쓰면 반복 조회를 줄일 수 있다. 그렇다고 폴링이 항상 불필요해지는 것은 아니다.

웹훅을 제공하지 않는 외부 서비스도 있을 수 있고, 우리 서버가 외부 요청을 받을 수 없는 환경도 있다. 또 웹훅이 누락되거나 순서가 뒤바뀌었을 때 조회 API로 현재 상태를 확인해야 할 수도 있다. Stripe도 이벤트의 전달 순서를 보장하지 않는다고 명시한다.[1]

따라서 중요한 상태를 연동한다면 ‘평소에는 웹훅으로 변화를 받고, 오래 처리 중인 항목은 조회 API로 확인한다’는 구성을 고려할 수 있다. 이는 웹훅이 놓친 결과를 복구하기 위한 설계 선택이다.

웹훅은 즉시 전달이나 한 번만 전달을 보장하는 특별한 통신이 아니다. HTTP 요청으로 변화를 알리는 방식이며, 전달 지연과 실패를 어떻게 다룰지는 별도로 정해야 한다.

정리

  1. API 응답이 요청 접수를 의미할 때는 최종 결과를 확인할 별도 경로가 필요하다.
  2. 폴링은 우리 서버가 결과를 반복해서 묻고, 웹훅은 외부 서비스가 변화를 알린다.
  3. 웹훅 수신에는 발신자 검증, 중복 처리 방지, 장애 후 복구를 함께 고려해야 한다.
  4. 웹훅과 조회 API를 함께 사용하면 누락되거나 늦어진 결과를 확인할 수 있다.

웹훅을 정의하는 문장보다, 최초 응답에 무엇이 담겨 있는지를 먼저 봐야 하는 이유를 알게 됐다.

다음에는 같은 완료 웹훅을 동시에 두 번 보냈을 때, ‘이미 처리했는지 확인하는 코드’만으로 중복을 막을 수 있는지 확인해보려 한다.

참고 자료

[1] Stripe — Receive Stripe events in your webhook endpoint: 비동기 이벤트, 서명 검증, 재전송, 중복 이벤트와 전달 순서에 관한 공식 문서.