카카오/토스 웹훅 연동할 때마다 ngrok 켜는 게 귀찮아서 만든 'K-API 웹훅 샌드박스'
안녕하세요, 개인 토이 프로젝트를 공유합니다.
FlashHook은 회원가입 없이 1초 만에 임시 웹훅 URL을 발급받는 개발자용 웹훅 샌드박스이자 Mock API 서비스입니다. 단순히 요청을 받아서 보여주는 것을 넘어, 카카오나 토스페이먼츠 같은 국내 주요 서비스의 실제 응답 스펙을 그대로 재현하는 데 초점을 맞췄습니다.
왜 만들었나
외부 API를 연동할 때마다 반복되던 두 가지 불편함이 출발점이었습니다.
상대방이 보내는 웹훅 페이로드를 확인하려면 매번
ngrok으로 터널을 열거나 서버를 미리 배포해야 했습니다.카카오 로그인이나 토스 결제 서버가 타임아웃·에러를 낼 때 내 서버가 잘 버티는지 검증하고 싶은데, 상대 서버를 고의로 망가뜨려 볼 방법이 없었습니다.
그래서 클릭 한 번으로 URL을 발급받아 실시간으로 요청을 확인하고, 반대로 "이 URL은 400 에러를 5초 지연 후에 돌려줘" 같은 걸 직접 세팅할 수 있는 도구를 만들었습니다.
무엇을 만들었나
1. Webhook Catcher
임시 URL을 발급하면 대시보드가 즉시 SSE로 연결되고, 요청이 들어오는 순간 Method, Headers, Body, Query가 폴링 없이 화면에 붙습니다. 나중에 로컬 서버가 꺼져 있어서 놓친 웹훅은 Replay API로 저장된 페이로드 그대로 재전송할 수 있습니다.
2. K-API Mock
FlashHook의 핵심 기능입니다. 국내 개발자가 자주 붙이는 6개 서비스(카카오, 토스페이먼츠, 포트원 V2, 솔라피, GitHub, Slack)의 공식 문서 기준 에러 코드와 응답 포맷을 프리셋으로 그대로 재현합니다.
몇 가지 예를 들면
카카오 OAuth는
KOE101(잘못된 REST API 키),KOE320(인가 코드 만료),KOE009(등록되지 않은 플랫폼) 같은 실제 에러 코드와 메시지 포맷을 그대로 뱉어줍니다.토스페이먼츠는
ALREADY_PROCESSED_PAYMENT(중복 승인),FAILED_PAYMENT_INTERNAL_SYSTEM_PROCESSING같은 결제 예외 케이스를 재현해 롤백/재시도 로직을 검증할 수 있습니다.Slack의 URL Verification처럼 요청마다 랜덤하게 오는
challenge값은 고정 응답으로 흉내 낼 수 없어서, 요청을 파싱해 그 값을 그대로 되돌려주는 동적 핸들러를 따로 구현했습니다.GitHub와 포트원 V2는 Replay 시점에
X-Hub-Signature-256,webhook-signature같은 서명을 실시간으로 생성해서 붙여 보내주기 때문에, 시그니처 검증 로직까지 테스트할 수 있습니다.
물론 상태 코드/지연 시간(최대 10초)/헤더/바디를 직접 조합하는 수동 Mock 설정도 지원합니다.
3. Replay API
과거에 받은 웹훅 로그를 골라 원하는 목적지로 다시 쏴줍니다. 서버가 사용자가 입력한 임의의 URL로 요청을 보내는 기능이라, 목적지가 사설 IP/루프백/링크 로컬 대역으로 해석되면 차단하고 DNS Rebinding 방지를 위한 IP Pinning도 적용했습니다.
기술적으로 신경 쓴 부분
인프라 비용 $0: OCI에 Spring Boot와 Redis를 올리고, MongoDB Atlas M0, 프론트는 Vercel, DNS/CDN은 Cloudflare로 구성해 도메인 유지비를 빼면 운영비 없이 돌아갑니다. 백엔드는 인바운드 포트(80/443)를 아예 열지 않고 Cloudflare Tunnel로만 외부 요청을 받는 Zero Trust 구조라 포트 스캐닝이나 IP 직접 공격 자체가 차단됩니다.
데이터 생명주기: 회원가입 없이 누구나 URL을 만들 수 있다 보니 스토리지 남용 걱정이 있었습니다. 그래서 모든 엔드포인트와 로그는 24시간 뒤 MongoDB TTL로 자동 파기되고, 그 전에도 엔드포인트당 최대 500건 또는 5MB를 넘으면 오래된 로그부터 순환 삭제되도록 앱 레벨 캡을 걸었습니다.
수신과 SSE 푸시의 완전한 분리: 웹훅을 저장하는 로직과 대시보드로 밀어주는 로직이 한 서비스 클래스에 섞이면, SSE 푸시 지연이 그대로 외부 서비스의 타임아웃으로 전파될 위험이 있습니다. 그래서 저장 후
ApplicationEvent를 발행하고@Async로 SSE 푸시만 분리했습니다. 저장 로직은 SSE를 모르고, 푸시 로직은 저장을 모릅니다.인증 및 시크릿 보호: 대시보드 접근용
accessToken은 서버에 원본이 아니라 SHA-256 해시로만 저장되고, GitHub나 포트원 시그니처 생성에 쓰는 시크릿 키는 AES-256으로 암호화해 저장하고 서명이 필요한 순간에만 복호화합니다.
배운 점
단순한 CRUD 웹훅 캐처라고 생각하고 시작했는데, 누구나 URL을 만들 수 있게 열어두는 순간 Rate Limit, TTL 기반 데이터 생명주기, 캡 적용 시점의 동시성까지 고민할 게 많아지더라고요. 그리고 webhook.site 같은 글로벌 서비스와 똑같이 빈 폼만 주기보다, 한국 개발자들이 실제로 자주 붙이는 결제, 인증 API의 스펙에 뾰족하게 파고드는 쪽이 훨씬 쓸모 있다는 것도 만들면서 느낀 부분입니다.
피드백 부탁드립니다
우선 여기까지 해서 Version 1을 마무리하려고 합니다. 웹훅 연동 테스트하실 때 한 번 써보시고, "이런 게 더 있으면 자주 쓸 것 같다"는 v2 아이디어나 쓴소리가 있으면 편하게 댓글 부탁드립니다. 긴 글 읽어주셔서 감사합니다!