고객이 결제하면, Stripe는 백엔드에서 payment_intent.succeeded 이벤트를 발생시키고, 귀하의 엔드포인트는 주문을 결제 완료로 표시해야 합니다. 이 마지막 단계는 조용히 실패하는 부분입니다. 웹훅이 도착하고, 핸들러에서 오류가 발생해도, "결제했는데 계정은 아직 미결제로 표시됩니다."라는 지원 티켓이 들어오기 전까지는 아무도 알아차리지 못합니다. 배포할 때마다 이벤트가 도착하여 올바르게 처리되었음을 증명하는 CI 테스트를 원할 것입니다.
까다로운 점은 웹훅이 귀하가 요청하는 것이 아니라 Stripe에서 귀하에게 들어오는 HTTP 호출이라는 것입니다. 대부분의 API 테스트 도구는 요청을 보내고 응답을 확인하도록 설계되어 있어, 이는 웹훅과 반대되는 형태입니다. 그래서 질문은 다음과 같습니다: CI 실행 중에 사람이 지켜보지 않는 상태에서, 자체 스케줄에 따라 도착하는 것에 대해 어떻게 단언(assert)할 수 있을까요? 이 가이드는 Apidog를 사용하여 이를 수행하는 정직하고 지원되는 방법을 보여주며, 미리 알아야 할 한계점부터 시작합니다. 이벤트 기반 엔드포인트 테스트에 대한 더 넓은 그림을 먼저 알고 싶다면, 웹훅 테스트 방법에 대한 저희 가이드가 바탕을 제공하며, Stripe의 웹훅 문서는 이벤트 전달 모델을 다룹니다.
주변에 설계해야 하는 제약 조건
Apidog 자체 문서에 명확히 명시된 핵심 사실은 다음과 같습니다: “Apidog는 웹훅 수신을 기본적으로 지원하지 않습니다.” Apidog는 공개 URL에 위치하여 Stripe의 인바운드 호출을 실시간으로 포착하지 않습니다. Stripe를 Apidog 리스너로 지정하고 이벤트가 들어오는 것을 지켜보려 했다면, 그러한 경로는 존재하지 않습니다.
그것은 막다른 길처럼 들립니다. 하지만 그렇지 않습니다. 단지 테스트의 형태를 바꿀 뿐입니다. 웹훅이 도착할 때 가로채는 대신, 백엔드에서 웹훅을 캡처하고 저장한 다음 Apidog가 해당 저장된 레코드를 쿼리하고 이를 단언(assert)하도록 합니다. 먼저 캡처하고, 그 다음 검증합니다. 이러한 분리를 받아들이면 전체 워크플로우가 간단해지고, 중요하게도 데이터베이스 쿼리는 확정적이고 반복 가능하므로 CI에 완벽하게 적합합니다.
캡처 후 쿼리 패턴은 어떻게 생겼는가
Apidog 문서에서 권장하는 패턴은 네 가지 움직이는 부분을 가지고 있습니다:
- 백엔드 서비스에 수신되는 Stripe 웹훅을 캡처할 엔드포인트를 생성합니다.
- 웹훅 이벤트 데이터를 데이터베이스의
Stripe 이벤트 로그테이블에 저장합니다. - Apidog의 Post-Request Processor를 사용하여 데이터베이스를 쿼리합니다.
- 저장된 웹훅 이벤트를 검색하고 예상 결과와 비교하여 유효성을 검사합니다.
이 단계 중 두 가지는 귀하의 코드에 있고, 두 가지는 Apidog에 있습니다. 캡처 엔드포인트와 로깅 테이블은 자체 애플리케이션 내에서 실행되므로 귀하가 구축해야 할 책임이 있습니다. Apidog의 작업은 이벤트가 데이터베이스에 들어간 후에 시작됩니다: Apidog는 해당 데이터베이스에 연결하여 행을 다시 읽어 이벤트가 예상대로 처리되었는지 확인합니다. 이 구분을 명확히 하면 나머지는 자연스럽게 진행됩니다.
1단계: 캡처 엔드포인트 구축
백엔드에는 Stripe가 POST할 수 있는 경로가 필요합니다. 이는 일반적인 애플리케이션 코드이며, Apidog 기능이 아닙니다. 서명을 확인하고 이벤트를 기록하는 최소한의 Express 핸들러는 다음과 같습니다:
import express from "express";
import Stripe from "stripe";
const app = express();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
app.post(
"/webhooks/stripe",
express.raw({ type: "application/json" }),
async (req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
req.headers["stripe-signature"],
endpointSecret
);
} catch (err) {
return res.status(400).send(`Signature check failed: ${err.message}`);
}
// 테스트가 나중에 읽어올 수 있도록 이벤트를 유지합니다.
await db.query(
`INSERT INTO stripe_event_logs (event_id, type, payload, handled_at)
VALUES ($1, $2, $3, now())
ON CONFLICT (event_id) DO NOTHING`,
[event.id, event.type, JSON.stringify(event.data.object)]
);
if (event.type === "payment_intent.succeeded") {
const intent = event.data.object;
await markOrderPaid(intent.metadata.order_id);
}
res.json({ received: true });
}
);
여기에는 두 가지 중요한 사항이 있습니다. 첫째, 아무것도 신뢰하기 전에 constructEvent로 Stripe 서명을 확인해야 합니다. 이는 모든 웹훅 수신자에게 필수적인 보안 단계입니다. 해당 확인의 전체적인 이유를 알고 싶다면, 웹훅 서명 검증에 대한 저희 설명이 원시 본문 비교가 이를 수행하는 유일하게 안전한 방법인 이유를 자세히 설명합니다. 둘째, 이벤트를 Stripe 이벤트 로그 테이블에 기록합니다. 이 행이 Apidog가 읽을 내용입니다. ON CONFLICT DO NOTHING 절은 Stripe가 동일한 이벤트를 두 번 이상 전달할 수 있으므로 로그를 멱등하게 유지합니다.
2단계: Apidog 환경에서 데이터베이스 연결
Apidog는 해당 환경의 데이터베이스 연결을 지원하며, 이 연결이 전체 패턴을 작동하게 합니다. CI 실행이 대상으로 하는 환경(스테이징 Postgres든 전용 테스트 데이터베이스든)에 대한 데이터베이스 연결을 설정합니다. 연결이 설정되면 테스트 단계에서 SQL을 실행하여 실제 행을 가져올 수 있습니다.
연결을 테스트 중인 환경과 일치시키세요. 스테이징에 대해 실행되는 테스트는 스테이징 데이터베이스를 쿼리해야 하므로, 테스트가 트리거하는 이벤트가 테스트가 읽는 이벤트가 됩니다. 환경 불일치는 통과하는 캡처 엔드포인트가 여전히 단언(assertion)에 실패하는 가장 흔한 원인입니다.
3단계: 로그를 쿼리하기 위한 Post-Request Processor 추가
이것이 핵심입니다. Post-Request Processor는 데이터베이스를 쿼리하고 테스트 내에서 기록된 웹훅 이벤트를 검증하는 Apidog 기능입니다. 테스트 시나리오의 요청에 이를 연결합니다. 요청이 실행된 후, 프로세서는 SQL을 실행하고 저장된 이벤트를 읽은 다음 결과에 대해 단언(assert)할 수 있도록 합니다.
payment_intent.succeeded 경우에 대한 현실적인 흐름:
- 귀하의 테스트 시나리오가 결제를 트리거합니다. 이는 Stripe 테스트 모드에서 결제 인텐트를 생성하고 확인하는 요청일 수도 있고, 또는 캡처 엔드포인트에 알려진 테스트 이벤트를 발생시키는 픽스처일 수도 있습니다.
- Stripe는 귀하의
/webhooks/stripe경로로 웹훅을 전달하며, 이는 서명을 확인하고stripe_event_logs에 행을 기록합니다. - 다음 단계의
Post-Request Processor는 해당 테이블에서 이벤트를 쿼리합니다.
프로세서가 실행하는 쿼리는 일반 SQL입니다:
SELECT event_id, type, payload, handled_at
FROM stripe_event_logs
WHERE type = 'payment_intent.succeeded'
ORDER BY handled_at DESC
LIMIT 1;
그런 다음 반환된 행에 대해 단언(assert)합니다. 로그된 데이터가 예상과 일치할 때 테스트는 통과합니다: type이 payment_intent.succeeded이고, event_id가 트리거한 것과 일치하며, payload 금액이 청구한 금액과 같고, handled_at이 채워져 있어 행이 플레이스홀더가 아니라 핸들러가 실제로 실행되었음을 증명합니다. 저장된 웹훅 이벤트를 검색하고 예상 결과와 비교한 다음 단언(assertion)이 통과 또는 실패를 결정하도록 합니다.
웹훅 전달 시점은 즉각적이지 않으므로 쿼리하기 전에 이벤트가 도착할 시간을 잠시 기다려주세요. 짧은 지연 단계나 실패하기 전에 쿼리를 몇 번 재시도하는 폴링 루프는 테스트가 Stripe의 전달 속도를 앞지르는 것을 방지합니다. 웹훅의 비동기적 특성이 테스트 설계에 스며드는 유일한 부분이며, 작은 재시도 간격이 이를 깔끔하게 처리합니다.
로컬 개발 중 실시간 전달에 대한 참고 사항
캡처 후 쿼리 패턴은 데이터베이스와 저장된 레코드가 바로 필요한 CI를 위해 구축되었습니다. 로컬 개발은 다른 상황입니다. 노트북에서 핸들러를 작성할 때, Stripe는 localhost에 직접 도달할 수 없으므로 이벤트를 실시간으로 귀하의 머신으로 전달할 무언가가 필요합니다.
이를 위해 Apidog 문서는 Stripe CLI와 Ngrok을 예로 들어 웹훅 릴레이 서비스를 언급합니다. Stripe CLI는 이벤트를 직접 로컬 포트로 수신하고 전달할 수 있습니다:
stripe listen --forward-to localhost:3000/webhooks/stripe
이는 핸들러를 구축하는 동안 라이브 이벤트를 제공합니다. Ngrok도 귀하의 로컬 포트를 Stripe 엔드포인트로 등록하는 공개 URL에 노출하여 동일한 작업을 수행합니다. 이를 내부 개발 루프에 사용한 다음, 파이프라인에서 실행되는 단언(assertion)을 위해 데이터베이스와 Post-Request Processor 흐름에 의존합니다. 이 두 가지는 상호 보완적입니다: 구축을 위한 릴레이, 증명을 위한 캡처 후 쿼리.
Apidog의 네이티브 웹훅 기능과 혼동하지 마세요
Apidog에는 실제로 웹훅(Webhook)이라고 불리는 기능이 있으며, 이를 통해 Stripe 이벤트를 포착할 수 있다고 쉽게 생각할 수 있습니다. 하지만 그렇지 않으며, 이를 혼동하면 시간을 낭비하게 될 것입니다. 네이티브 웹훅 기능은 아웃바운드 웹훅을 정의하고 문서화하기 위한 것으로, 이는 이벤트 발생 시 시스템이 호출하는 HTTP 엔드포인트를 의미합니다. 시스템이 외부 URL로 호출을 시작하며, 이는 클라이언트가 귀하를 호출하는 일반 엔드포인트와 반대입니다. 이는 API 문서에서 상태 변경 알림 및 비동기 작업 결과를 설명하는 데 사용되며, Stripe의 인바운드 호출을 수신하는 데 사용되지 않습니다.
자신의 아웃바운드 웹훅 중 하나를 문서화하고 싶다면, 흐름은 간단합니다:
- 왼쪽 사이드바에서
+아이콘을 클릭합니다. 새로운 기타 프로토콜 API(New Other Protocol APIs)를 선택한 다음웹훅(Webhook)을 선택합니다.- 필수 필드를 채웁니다:
요청 메서드(Request Method)(일반적으로 POST),웹훅 이름(Webhook Name), 테스트용 선택적디버그 URL(Debug URL), 그리고 요청 본문, 헤더 및 구성을 위한기타 정보(Other Info). 저장(Save)을 클릭합니다.
시도하려면 디버그 URL(Debug URL) 필드에 URL을 입력하고 전송(Send)을 클릭하여 웹훅 호출을 시뮬레이션합니다. 기억해야 할 한 가지 주의사항은 디버그 URL(Debug URL)은 테스트 전용이며 게시된 문서나 OpenAPI 내보내기에는 나타나지 않습니다. 이벤트 콜백 설계 및 문서화에 대한 더 자세한 내용은 API 설계의 웹훅에 대한 저희 글에서 해당 내용이 어디에 적합한지 다룹니다. 이 글의 요약: 네이티브 웹훅 기능은 아웃바운드 이벤트를 정의하고, 캡처 후 쿼리 패턴은 Stripe의 인바운드 이벤트를 검증합니다. 이 두 가지를 별도의 개념으로 유지하세요.
변형 및 강화
기본 단언이 작동하면 몇 가지 개선 사항이 이를 프로덕션 수준으로 만듭니다. 첫째, 이벤트 유형 이상에 대해 단언합니다. `event_id`를 처음부터 끝까지 확인하여 트리거한 정확한 이벤트가 검증한 이벤트이지, 이전 실행에서 남은 것이 아님을 알 수 있습니다. 이벤트가 쌓이면 테스트 실행마다 `stripe_event_logs` 테이블을 잘라내거나 범위를 지정합니다.
둘째, 실패 경로를 테스트합니다. 핸들러가 거부해야 하는 이벤트(예: 잘못된 서명 또는 예상치 못한 유형)를 발생시키고 handled_at 타임스탬프가 기록되지 않음을 단언합니다. 해피 경로만 확인하는 웹훅 테스트 스위트는 실제로 새벽 2시에 호출하는 경우를 놓칩니다. 결제 웹훅 모범 사례에 대한 저희 글은 이러한 테스트에 인코딩할 가치가 있는 멱등성 및 재시도 동작을 다룹니다.
셋째, 단언(assertion)을 전달뿐만 아니라 비즈니스 의미에 밀접하게 유지하세요. "이벤트가 도착했다"는 "주문이 결제 완료 상태로 변경되었다"는 것보다 약합니다. 핸들러가 orders 테이블을 업데이트하는 경우, 다운스트림 상태가 변경되었음을 확인하는 두 번째 쿼리를 추가하여 테스트가 단순히 로그 기록뿐만 아니라 전체 체인을 증명하도록 합니다.
이를 병합 게이트를 넘어 확장할 수도 있습니다. 시나리오가 Apidog에 저장되면, 이를 주기적으로 실행하도록 예약하여 배포 사이에도 손상된 웹훅 핸들러가 드러나도록 할 수 있습니다. Apidog에서 API 테스트를 예약하는 방법에 대한 저희 가이드는 동일한 유효성 검사를 타이머에 어떻게 설정하는지 보여줍니다.
Apidog CLI를 사용하여 워크플로우 자동화
위에서 언급한 모든 것은 무인으로 실행될 때 그 진가를 발휘하며, 바로 이 지점에서 Apidog CLI가 등장합니다. 이는 본질적으로 CI 이야기이므로, 저장된 시나리오를 파이프라인에 연결하는 것이 자연스러운 마무리입니다. CLI를 설치하고 토큰으로 인증하세요:
npm install -g apidog-cli
apidog login --with-token <YOUR_ACCESS_TOKEN>
그런 다음 이벤트 로그가 저장된 데이터베이스를 가진 환경에 대해 저장된 웹훅 유효성 검사 시나리오를 헤드리스로 실행합니다:
apidog run --access-token $APIDOG_ACCESS_TOKEN -t <SCENARIO_ID> -e <ENV_ID> -r cli
여기서 -t는 테스트 시나리오 ID, -e는 환경 ID, -r은 리포터를 선택합니다. CI 아티팩트의 콘솔 출력과 함께 브라우저에서 볼 수 있는 보고서를 원한다면 -r html,cli를 사용하세요. 시나리오에는 Post-Request Processor와 데이터베이스 쿼리가 포함되어 있으므로, 단일 명령으로 흐름을 트리거하고 stripe_event_logs 행을 읽으며, 단언(assertion)이 실패하면 0이 아닌 종료 코드를 반환합니다. 이는 파이프라인이 병합을 차단하는 데 정확히 필요한 것입니다. Apidog CLI 설치 가이드는 토큰 설정을 다루고, CI/CD 파이프라인 둘러보기는 이 명령을 둘러싼 전체 GitHub Actions 연결 방법을 보여줍니다.
자주 묻는 질문
Apidog가 Stripe 웹훅을 직접 수신할 수 있나요? 아니요. Apidog 문서에서는 "웹훅 수신을 기본적으로 지원하지 않습니다."라고 명확히 밝히고 있습니다. 이벤트를 자체 백엔드 엔드포인트에서 캡처하고 데이터베이스에 저장하면 Apidog가 Post-Request Processor를 사용하여 이를 다시 읽어옵니다. 로컬 개발 중 실시간 전달의 경우, Stripe CLI나 Ngrok과 같은 릴레이를 사용하세요.
단언(assertion)은 실제로 어디서 이루어지나요? 테스트 시나리오의 요청에 있는 Post-Request Processor 단계 내부에서 이루어집니다. 이는 환경에 구성된 데이터베이스 연결을 통해 Stripe 이벤트 로그 테이블을 쿼리하고, 저장된 이벤트를 검색한 다음 예상 값과 비교합니다. 로그된 데이터가 일치하면 테스트가 통과합니다.
데이터베이스 유효성 검사 흐름을 위해 유료 플랜이 필요한가요? 이 워크플로우에 대한 Apidog 문서에는 어떤 플랜 제한도 명시되어 있지 않으므로, 이 가이드에서 이를 만들어내지 않겠습니다. 솔직한 답변은 가격 페이지에서 현재 플랜 세부 정보를 확인하는 것입니다. Apidog를 다운로드하고 테스트 프로젝트를 설정하여 Post-Request Processor와 환경 데이터베이스 연결을 직접 확인할 수 있습니다.
트리거링과 전달 사이의 지연은 어떻게 처리해야 하나요? 웹훅 전달은 즉각적이지 않으므로, 쿼리 전에 짧은 대기 시간이나 폴링 재시도를 추가하여 테스트가 Stripe의 전달보다 먼저 실행되지 않도록 하세요. 몇 초에 걸쳐 몇 번 재시도하는 것이 보통 충분합니다. 비동기 엔드포인트에 대한 단언(assertion)이 처음이라면, Stripe의 세부 사항을 추가하기 전에 일반적인 웹훅 테스트 방법 가이드부터 시작하세요.
네이티브 웹훅 기능이 여기에서 전혀 유용한가요? Stripe 이벤트를 캡처하는 데는 유용하지 않습니다. 이 기능은 시스템이 외부 URL을 호출하는 자체 아웃바운드 웹훅을 정의하고 문서화합니다. 이는 문서화 및 설계 도구이며, 이 문서에서 사용하는 인바운드 캡처 후 쿼리 패턴과는 별개입니다. 이 두 가지를 명확히 구분하세요.
마무리
Stripe를 Apidog로 지정하고 이벤트를 실시간으로 포착할 수는 없으며, 그렇지 않다고 가정하면 답답한 오후를 보내게 될 것입니다. 지원되는 경로는 처음 보이는 것보다 더 깔끔합니다: 웹훅을 자체 엔드포인트에서 캡처하고, Stripe 이벤트 로그 테이블에 기록한 다음, Apidog의 Post-Request Processor가 해당 레코드를 쿼리하고 이벤트가 처리되었음을 단언하도록 합니다. 저장된 시나리오를 apidog run으로 감싸면, 매 병합마다 실제 결제 이벤트가 주문을 결제 완료 상태로 변경했음을 파이프라인이 증명합니다. 신용카드 없이 무료로 사용해보고, 가장 중요한 웹훅 뒤에 실제 단언(assertion)을 적용해보세요.
