자동화 slecs

공유 결제 웹훅에서 외부 주문 이벤트 걸러내기

목차

공유 조직 웹훅에서 들어오는 외부 주문을 걸러내는 작업을 했다.

배경: 공유 org 웹훅의 함정

Polar를 결제/구독 인프라로 쓰다 보면 자연스럽게 조직(organization) 단위로 웹훅을 연결하게 된다. 근데 이 조직이 여러 프로젝트 혹은 여러 서비스에 걸쳐 공유되는 구조라면 얘기가 달라진다. 웹훅 엔드포인트는 하나인데, 거기로 들어오는 이벤트는 내 서비스가 만든 주문만이 아니라는 거다.

같은 org 안에 다른 팀의 제품, 다른 프로젝트의 주문, 심지어 테스트 결제까지 전부 같은 웹훅 스트림으로 흘러 들어온다. 처음엔 이 부분을 크게 신경 안 썼는데, 운영하다 보니 내 서비스와 무관한 주문 이벤트가 처리 로직에 섞여 들어오면서 오류 로그가 쌓이기 시작했다. 처리할 수 없는 주문 ID를 조회하다 실패하거나, 없는 사용자와 매핑하려다 예외가 터지는 식이었다.

이 문제는 사실 웹훅 설계에서 꽤 흔하게 마주치는 유형이다. 멀티 테넌트 구조이거나, 외부 서비스를 공유 계정으로 연결할 때 항상 "이 이벤트가 내 것인지 아닌지"를 먼저 판별하는 게이트가 필요하다.

뭘 바꿨나

변경된 파일은 두 곳이다.

파일 역할 이번 변경 의미
src/app/api/polar/webhook/route.ts 웹훅 요청을 받아 이벤트 타입별로 분기하는 진입점 foreign order 필터링 로직 추가
src/lib/polar.ts Polar API 클라이언트 래퍼, 주문/구독 관련 유틸 내 서비스 주문 여부를 판별하는 헬퍼 함수 정의

route.ts 쪽은 웹훅 이벤트를 받자마자 주문이 내 서비스 소속인지 먼저 체크하고, 아니면 조용히 200 OK만 돌려주고 빠져나오는 구조로 바꿨다. 200을 돌려주는 게 포인트인데, 에러로 응답하면 Polar 쪽에서 재시도를 계속 날려버리기 때문이다. "받았어, 근데 나랑 관계없어"를 표현하는 게 200 + early return이다.

// route.ts 패턴 (개념적 예시)
const order = event.data;

if (!isOwnOrder(order)) {
  // foreign org의 주문 — 조용히 무시
  return new Response(null, { status: 200 });
}

// 이후 내 서비스 처리 로직
await handleOrderFulfillment(order);

polar.ts에서는 주문 메타데이터나 product ID 같은 식별자를 기준으로 "이 주문이 내 서비스에서 생성된 것인지" 판단하는 헬퍼를 뽑았다. 이걸 분리해놓으면 나중에 판별 기준이 바뀌어도 route.ts를 건드릴 필요 없이 polar.ts만 수정하면 된다. 작은 SRP(단일 책임 원칙)지만, 이런 류의 로직은 시간이 지나면서 조건이 복잡해지는 경향이 있어서 처음부터 분리해두는 게 낫다고 판단했다.

회고

이걸 고치면서 다시 한 번 느낀 건, 외부 서비스 웹훅을 연결할 때 "내 이벤트만 들어온다"는 가정을 초기에 깔고 가면 반드시 어느 시점에 터진다는 거다. 공유 인프라, 공유 계정, 멀티 테넌트 구조가 점점 많아지는 요즘엔 웹훅 수신 단에서 항상 아이덴티티 체크를 첫 단계로 넣어야 한다.

비슷한 패턴의 예를 들면:
- Stripe connect 계정 웹훅에서 account 파라미터로 자기 플랫폼 계정 여부 확인
- GitHub App 웹훅에서 installation ID 또는 repository owner 필터링
- Slack 이벤트 API에서 team_id 검증

다 결국 같은 맥락이다. "이 이벤트, 내 거 맞아?" 를 제일 먼저 묻는 것.

핀포인트 수정이라 diff 자체는 크지 않았지만, 이 한 줄 짜리 early return이 없어서 조용히 쌓이던 오류 로그가 깔끔하게 사라졌다. 작은 변경인데 운영 안정성에 직결되는 류의 수정이라 오히려 더 신경 써서 검토했다.


🛒 이 글과 어울리는 추천 상품

*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.

댓글 0

첫 댓글 달아줘.