조용한 $800 MRR 킬러: 내가 BillingWatch를 만든 이유

발행: (2026년 3월 27일 PM 01:46 GMT+9)
6 분 소요
원문: Dev.to

I’m happy to translate the article for you, but I’ll need the full text of the post (everything after the source line) in order to do so. Could you please paste the article’s content here? Once I have it, I’ll provide a Korean translation while preserving the original formatting, markdown, and any code blocks or URLs.

내가 하락을 눈치챈 날

거의 무시할 뻔한 Slack 알림에서 시작되었다: “Stripe: charge failed.”
한 번의 결제 실패—특별히 이상한 점은 없었다. 나는 그것을 넘기고 코딩을 계속했다.

삼일 뒤, 대시보드를 검토하던 중 MRR이 $800 이상 급락한 것을 발견했다. 점진적인 감소가 아니라 절벽 같은 하락이었다. 내가 기능을 개발하는 동안 조용히 떠난 구독자가 여덟 명이나 있었다. 결제 실패가 계속 쌓였고, Stripe가 재시도했지만 모두 실패했으며, 내가 사용하는 어떤 도구에서도 알림이 한 번도 울리지 않았다.

그때 나는 BillingWatch를 만들기 시작했다.

Source:

누락된 패턴

Stripe 대시보드에서는 이벤트를 표시하지만 패턴이 잘못됐을 때는 알려주지 않습니다. 제가 놓친 것은 다음과 같습니다:

  • 중복 청구 – 같은 고객, 같은 금액, 60초 이내. Stripe 재시도는 공격적이며, 멱등성 키(idempotency keys)를 잘못 설정하기 쉽습니다.
  • 청구 실패 연쇄 – 한 번의 카드 실패가 아니라 한 시간에 다섯 번. 이는 “불량 카드”가 아니라 카드 테스트 공격입니다.
  • 만료된 구독customer.subscription.updated 이벤트가 구독이 past_due 상태로 전환될 때 발생합니다. 필터링은 쉽지만 며칠 동안 놓치기 쉽습니다.
  • 음수 청구서 이상 – 예상치 못한 크레딧이 음수 라인 항목을 생성합니다. 의도된 경우는 괜찮지만, 그렇지 않다면 조용한 버그가 됩니다.

이러한 이벤트는 깔끔하게 구독할 수 있는 이벤트가 아닙니다. 이벤트 전반에 걸친 패턴이며, 패턴을 감시하려면 모니터가 필요합니다.

BillingWatch 아키텍처

BillingWatch는 FastAPI 서비스로, Stripe 웹훅 엔드포인트 앞에 위치합니다. 모든 이벤트는 애플리케이션이 처리하기 전에 탐지 엔진을 통과합니다.

# webhook.py
from fastapi import FastAPI, Request
from billingwatch.detectors import run_all_detectors
from billingwatch.store import save_event
import stripe

app = FastAPI()

@app.post("/webhook")
async def stripe_webhook(request: Request):
    payload = await request.body()
    event = stripe.Webhook.construct_event(
        payload,
        request.headers["stripe-signature"],
        STRIPE_WEBHOOK_SECRET,
    )

    # Store first, then detect
    await save_event(event)
    alerts = await run_all_detectors(event)

    if alerts:
        await send_alerts(alerts)

    return {"received": True}

탐지기들은 조합이 가능합니다. 각 탐지기는 이벤트를 받아 알림 목록(또는 아무것도) 반환합니다. 과거 이벤트를 살펴 패턴을 찾을 수도 있습니다.

예시 탐지기: 중복 결제

# detectors/duplicate_charge.py
import time
from typing import List

class DuplicateChargeDetector:
    async def detect(self, event: dict) -> List[Alert]:
        if event["type"] != "charge.succeeded":
            return []

        charge = event["data"]["object"]
        window_start = time.time() - 60  # 60‑second window

        recent = await get_charges(
            customer=charge["customer"],
            amount=charge["amount"],
            since=window_start,
        )

        if len(recent) > 1:
            return [
                Alert(
                    level="warning",
                    message=(
                        f"Duplicate charge detected: {charge['customer']} "
                        f"charged ${charge['amount']/100:.2f} twice in 60s"
                    ),
                    event_id=event["id"],
                )
            ]
        return []

이 코드를 모든 charge.succeeded 이벤트에 적용하면 중복을 절대 놓치지 않게 됩니다.

실제 현장의 침묵 살인자

다섯 명의 SaaS 창업자와 이야기를 나눴는데, 모두 각기 다른 “침묵 살인자”를 가지고 있었습니다:

  • 내부에서 예외가 발생했지만 200을 조용히 반환한 웹훅 엔드포인트.
  • 신용카드가 만료된 후에도 90 일 동안 구독이 계속 활성 상태였음.
  • 카드 테스트 공격이 몇 주 동안 감지되지 않음.

BillingWatch는 가장 흔한 패턴에 대한 탐지기를 기본 제공하지만, 진정한 가치는 비즈니스에 특화된 실패 모드에 대해 직접 탐지기를 작성할 수 있다는 점입니다.

핵심 원칙

지표만이 아니라 이벤트를 모니터링하세요.
지표는 무언가 잘못되었다는 것을 알려줍니다. 이벤트 패턴은 그런지 알려주며—종종 지표에 나타나기 전에 이를 포착합니다.

오픈 소스

BillingWatch는 오픈 소스입니다. Stripe를 어떤 규모로 운영하든 웹훅 스트림에서 이상 징후를 감시하지 않으면 눈이 먼 채로 비행하는 겁니다. 저는 필요해서 직접 만들었습니다. 여러분도 그럴 가능성이 높습니다—이미 작동하는 것을 사용하는 것이 좋습니다.

BillingWatch on GitHub

0 조회
Back to Blog

관련 글

더 보기 »