티스토리 뷰

반응형

쿠버네티스 실습: SaaS 청구·과금(Billing) 시스템 구축

테넌트별 API 사용량 → 요금 항목 변환 → Stripe 자동 결제 파이프라인

앞선 글에서는 테넌트별 Rate Limit & Quota Enforcement를 구축해
SaaS 서비스의 핵심 기능인 “사용량 통제”를 안정적으로 완성했습니다.

이제 실제 SaaS 비즈니스의 필수 마지막 레이어,
Billing(과금) & Subscription(정기 구독) 시스템을 구축합니다.

목표는 다음과 같습니다.

“테넌트별 API 사용량을 정확하게 계산하여 요금 항목으로 변환하고,
Stripe와 연동해 자동 청구·결제·영수증 발행까지 구현한다.”

실제 SaaS 서비스가 운영되는 방식 그대로 따라가는 실습입니다.


1. 전체 아키텍처

[Istio Gateway logs / Nest API logs / Redis Stream (API usage)]
          │
          ▼
[Aggregator Worker (NestJS or Go)]  ← 5분/1시간 단위 집계
          │
          ▼
[Billing DB (PostgreSQL + Prisma)]  ← 테넌트별 사용량 저장
          │
          ▼
[Billing Engine] ← 요율(Price Plan) 적용
          │
          ▼
[Stripe] ← 결제/구독/영수증 발행/세금

Billing의 핵심은 3단계입니다:

1) Usage 수집  
2) Usage → 요금 계산  
3) Stripe로 자동 청구

2. 테넌트별 Price Plan 구조 설계

Prisma 기반 Billing 스키마:

model Tenant {
  id              String      @id @default(uuid())
  name            String
  stripeCustomerId String?
  plans           Plan[]
  usages          Usage[]
  invoices        Invoice[]
}

model Plan {
  id               String @id @default(uuid())
  tenantId         String
  type             String   // basic, pro, enterprise
  pricePerRequest  Float    // 예: 0.001 USD
  monthlyBaseFee   Float    // 기본요금 예: 29 USD
  freeTierRequests Int      // 예: 10,000 free
  Tenant Tenant @relation(fields: [tenantId], references: [id])
}

model Usage {
  id        String @id @default(uuid())
  tenantId  String
  timestamp DateTime @default(now())
  count     Int
}

model Invoice {
  id           String @id @default(uuid())
  tenantId     String
  periodStart  DateTime
  periodEnd    DateTime
  amount       Float
  stripeInvoiceId String?
}

이 스키마는 SaaS 시장에서 흔히 쓰는 구조(Segment, Stripe Metered Billing)를 그대로 차용했습니다.


3. API Usage 집계 (Redis Stream → Batch Aggregator)

반응형

Redis Stream에서 사용량을 1분 혹은 5분 단위로 집계합니다.

usage_worker.ts

import Redis from "ioredis";
import { PrismaClient } from '@prisma/client';

const redis = new Redis(process.env.REDIS_URL);
const prisma = new PrismaClient();

async function aggregate() {
  const records = await redis.xrange("api_stream", "-", "+");

  const usageMap = new Map<string, number>();

  for (const [id, fields] of records) {
    const tenant = fields[1];
    usageMap.set(tenant, (usageMap.get(tenant) || 0) + 1);
  }

  for (const [tenant, count] of usageMap.entries()) {
    await prisma.usage.create({
      data: {
        tenantId: tenant,
        count,
      },
    });
  }

  // Clear stream
  await redis.xtrim("api_stream", "MAXLEN", 0);
}

aggregate().finally(() => process.exit());

이 워커는 CronJob 또는 KEDA ScaledJob으로 실행합니다.


4. Price Plan을 반영한 요금 계산 엔진

핵심 로직

청구금액 = 기본요금 + max(0, (총요청수 - 무료요청수)) × 단가

billing_engine.ts

export async function calculateInvoice(tenantId: string, start: Date, end: Date) {
  const plan = await prisma.plan.findFirst({ where: { tenantId } });
  const usages = await prisma.usage.findMany({
    where: { tenantId, timestamp: { gte: start, lt: end } }
  });

  const total = usages.reduce((sum, u) => sum + u.count, 0);
  const billable = Math.max(0, total - plan.freeTierRequests);

  const amount = plan.monthlyBaseFee + (billable * plan.pricePerRequest);

  return { total, billable, amount };
}

5. Stripe 연동: Customer 생성

테넌트 생성 시 Stripe Customer를 자동으로 만듭니다.

import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET, { apiVersion: "2023-10-16" });

async function createCustomer(tenantId: string, email: string) {
  const customer = await stripe.customers.create({
    email,
    metadata: { tenantId }
  });

  await prisma.tenant.update({
    where: { id: tenantId },
    data: { stripeCustomerId: customer.id }
  });

  return customer.id;
}

6. Stripe Invoice 생성

async function createStripeInvoice(tenantId: string, amount: number) {
  const tenant = await prisma.tenant.findUnique({ where: { id: tenantId }});
  if (!tenant?.stripeCustomerId) throw new Error("No customer ID");

  const invoiceItem = await stripe.invoiceItems.create({
    customer: tenant.stripeCustomerId,
    amount: Math.round(amount * 100), // USD → 센트
    currency: "usd",
    description: `API usage billing`,
  });

  const invoice = await stripe.invoices.create({
    customer: tenant.stripeCustomerId,
    auto_advance: true, // 자동 청구
  });

  return invoice.id;
}

7. 매월 자동 청구 CronJob (Kubernetes)

apiVersion: batch/v1
kind: CronJob
metadata:
  name: monthly-billing
  namespace: billing
spec:
  schedule: "0 0 1 * *" # 매월 1일 00시
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: billing-worker
            image: ghcr.io/your/billing-engine:latest
            env:
            - name: STRIPE_SECRET
              valueFrom:
                secretKeyRef:
                  name: stripe-secret
                  key: key
          restartPolicy: OnFailure

실제로 Stripe는 매월 자동 인보이스 발송, 영수증 발행, 재시도까지 처리합니다.


8. 테넌트 포털에서 “청구내역” 조회 API

billing.controller.ts

@Get('invoices')
async getInvoices(@Req() req) {
  const tenantId = req.tenantId;
  return prisma.invoice.findMany({
    where: { tenantId },
    orderBy: { periodStart: "desc" }
  });
}

9. 단가에 따른 다양한 요금 모델 구현

SaaS에서 흔히 쓰는 과금 모델도 쉽게 추가 가능:

모델 설명

Tiered 10k까지 0.0001 USD, 이후 0.00005 USD
Volume 전체 사용량에 따라 단가 자동 변경
Flat + Metered 기본 구독료 + 사용량
Premium Endpoint 요율 특정 API만 가중치 부여

예시: Premium API 단가

if (endpoint === "/v1/ocr") {
  billable += 3; // 3배 가격
}

10. Fraud / Abuse 방어

Stripe + RateLimit + Quota + Fraud Detection을 결합하면 완전한 상업용 인프라:

  • stolen API key → 즉시 사용량 급증 → RateLimit + Loki Alert
  • 지나친 요청 폭주 → AIOps 예측 모델로 early throttle
  • 고액 청구 → Stripe Radar로 사기 방지

11. 운영 시나리오

상황 동작

API 과다 사용 RateLimit → Quota → Stripe 청구 증가
월말 사용량 집계 CronJob이 Invoice 생성
결제 실패 Stripe 재시도 + failover Webhook
테넌트 구독 변경 Price Plan 업데이트 → 다음 달부터 반영
고액 API 폭주 AIOps 기반 예측 차단

12. 정리

이번 글에서 구축한 Billing 시스템은 실제 SaaS 기업이 사용하는 방식 그대로입니다.

  • 테넌트별 API 사용량 수집
  • Redis Stream → Worker 집계
  • Prisma 기반 Usage/Invoice 저장
  • Price Plan에 따라 요금 계산
  • Stripe Customer/Invoice/Payment 자동 처리
  • 월별 CronJob 실행
  • Fraud 방어, Premium API 지원, 대량 API 구독 모델까지 확장

그리고 이 아키텍처는 앞에서 구축한
RateLimit, Multi-Tenant, AIOps, Observability, Zero Trust 보안 체계
유기적으로 결합됩니다.


다음 글 예고

다음 편에서는 이 SaaS 시스템에 “관리 콘솔(Admin Console)” 을 구축합니다.

관리자가 GUI로

  • 테넌트 생성
  • Price Plan 설정
  • API Key 발급/취소
  • 사용량 그래프
  • 청구 내역
  • 보안 정책/RateLimit 정책 조정

까지 모두 관리할 수 있는 SaaS 운영 대시보드(NestJS + NextJS + Grafana API) 를 만듭니다.


 

쿠버네티스,SaaS,Billing,Stripe,Prisma,RedisStream,UsageBasedBilling,Subscription,RateLimit,K8s실습

 

 


✅ 참고할 최신 자료

  • Stripe 공식 가이드: SaaS 과금 모델(구독, 사용량 기반 등)을 설명해 있으며, “요금 모델 선택”, “자동 청구”, “안정적 수익 구조” 측면을 잘 정리하고 있습니다. (Stripe)
  • Stripe 블로그: SaaS 지불 처리에서 직면하는 문제 및 해결책을 설명한 글 (예: 반복 결제, 실패 대응, 지역별 과금) (Stripe)
  • Stripe 활용 사례: 사용 기반 요금 모델(usage-based billing)에서의 설계와 유의사항에 대해 정리되어 있습니다. (iteratorshq.com)

⚠️ 보완 / 강화 제안

아래 항목들을 글에 추가하시면 독자가 실무 구현 시 놓치기 쉬운 부분까지 대비할 수 있어요.

  • 요금 모델 선택 및 구조화
    글에서 기본요금 + 사용량 단가 구조를 예시로 들었는데, 추가로 “Tiered Pricing”, “Volume Pricing”, “Add-on 기반 요금” 등 실제 SaaS에서 자주 쓰이는 모델을 간단히 정리하면 좋습니다. 참고자료에서도 이 부분이 강조되어 있어요. (iteratorshq.com)
  • 청구 처리 및 실패 대응 흐름
    예: 결제 실패 시 재시도, 카드 유효기간 만료, 구독 해지 시의 처리, 영수증 발행과 세금 계산. Stripe 관련 자료에서 이 부분이 중요하게 다뤄집니다. (Stripe)
  • 사용량 집계 정확성 및 지연 고려
    API 사용량을 Redis Stream → Worker로 집계하는 구조인데, 실제 운영에서는 이벤트 누락, 중복 집계, 지연(latency) 등이 문제가 됩니다. 이 부분을 “데이터 청소(Cleaning) + 중복 제거 + 타임존 고려” 등으로 보완하면 좋습니다.
  • 투명성 있는 과금 제공
    고객(테넌트)에게 과금 내역을 명확히 보여주는 것도 중요합니다 — “사용량 단위(예: API 호출 수) vs 청구 금액” 비교, 무료 티어, 할인 적용 등. Stripe 자료에서도 “사용 단위 정의(clear usage unit)”가 강조되어 있어요. (iteratorshq.com)
  • 보안/컴플라이언스 측면
    결제정보, 고객 정보가 포함되므로 PCI-DSS, 지역별 세금(예: 부가가치세 VAT) 등 준수사항을 간단히 언급하면 신뢰도가 올라갑니다.
  • 테넌트별 과금 자동화 및 확장 고려사항
    다수의 테넌트를 운용할 때 “사용량 증가 → 요금 상승”이 자동으로 반영되도록 설계해야 하며, 테넌트별 과금 이력 저장, 과금 리포트 제공, 테넌트별 요금 정책 변경(업그레이드/다운그레이드) 대응 흐름이 들어가면 더 좋습니다.

 

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함
반응형