프로젝트 목록으로 돌아가기
Recruiter Briefai-usage-billing-gateway

AI Usage Billing Gateway

멀티테넌트 AI API Gateway에서 tenant isolation, usage idempotency, webhook duplicate handling, ledger balance를 테스트로 검증했습니다.

Role
백엔드 설계 및 구현
Period
2026
Team
개인 프로젝트
Theme
Billing
Java 21Spring Boot 3Spring SecuritySpring Data JPAPostgreSQLRedisFlywayJWT

30초 요약

문제
멀티테넌트 과금에서 tenant boundary, usage retry, webhook duplicate, ledger 불변식이 깨질 수 있었습니다.
해결
API key hash storage, idempotency key, providerEventId reserve, append-only ledger, audit hygiene를 적용했습니다.
검증
tenant isolation, usage/webhook idempotency, ledger balance를 검증했고 k6 결과는 Pending으로 분리했습니다.

Problem

Problem

AI API 사용량 과금 흐름은 사용자 인증, 조직 권한, API key, usage event, quota, invoice, webhook, ledger, audit가 한 요청 경계 안에서 이어져 작은 중복도 과금 오류로 번질 수 있습니다.

Decision

Decision

성능 수치를 먼저 내세우기보다 tenant isolation과 retry idempotency, webhook duplicate delivery, ledger consistency처럼 깨지면 치명적인 정합성 문제를 테스트로 묶었습니다.

Build

Implementation

  • organization membership 기반 TenantAccessService로 organization-scoped API를 보호하고 OWNER/ADMIN/MEMBER 권한을 분리했습니다.
  • API key는 raw value를 생성 시 한 번만 반환하고 DB에는 key prefix와 hash만 저장합니다.
  • usage event는 organizationId + Idempotency-Key unique scope와 request hash로 duplicate와 conflict를 구분합니다.
  • invoice는 organizationId + billingPeriod 기준으로 idempotent하게 생성하고, payment webhook은 providerEventId와 payload hash로 중복 delivery를 처리합니다.
  • ledger entry와 audit log는 append-only 성격으로 두고, audit metadata에는 raw API key나 secret을 남기지 않습니다.

과금 도메인은 빠른 API보다 먼저 tenant boundary, idempotency, duplicate webhook, ledger invariant를 설명할 수 있어야 신뢰도가 높아진다는 점을 보여주는 대표작입니다.

Proof

Verification

README와 docs/DESIGN.md의 검증 표, docs/PERF_RESULT.md의 Verified/Pending 구분을 기준으로 기능 정합성은 Verified, k6 mixed usage result는 Pending으로 분리했습니다.

tenant isolation
Verified
Tenant Boundary

membership/RBAC 기반 organization-scoped API 접근 제어

idempotency
Verified
Usage & Webhook

usage request hash와 providerEventId duplicate/conflict 처리

k6 pending
Pending
Load Test

No load test has been executed yet

Verified

tenant isolation verified

Tenant / API Key

Scenario
다른 organization 데이터 접근과 raw API key 저장 위험
Method
AuthTenantSecurityIT, ApiKeyUsageQuotaIT로 membership, RBAC, raw one-time display, hash storage 검증
Result
organization-scoped API 접근 제어와 API key 원문 미저장을 확인했습니다.
Verified

idempotency verified

Usage / Webhook

Scenario
usage retry, 같은 idempotency key의 payload mismatch, webhook duplicate delivery
Method
UsageServiceTest, ApiKeyUsageQuotaIT, BillingPaymentLedgerAuditIT, PaymentWebhookServiceTest로 중복/충돌 정책 검증
Result
organizationId + Idempotency-Key unique scope, request hash 비교, providerEventId duplicate/conflict 처리를 검증했습니다.
Verified

ledger balance verified

Ledger / Audit

Scenario
invoice/payment 상태를 ledger entry와 audit log로 설명해야 하는 과금 흐름
Method
BillingPaymentLedgerAuditIT, LedgerServiceTest로 debit/credit balance, single currency, positive amount invariant 검증
Result
append-only ledger와 audit log secret hygiene를 확인했습니다. simplified balanced-entry model로만 설명합니다.
Pending

k6 result pending

Load Test

Scenario
mixed usage k6 script
Method
k6 scenario added and inspectable
Result
No load test has been executed yet. Throughput, latency, failure 비율은 측정값으로 쓰지 않습니다.

Boundaries

Trade-offs & Limitations

Trade-offs

  • DB unique constraint 기반 idempotent storage는 중복 저장을 막지만 분산 시스템 전체의 exactly-once 처리는 주장하지 않습니다.
  • Redis fixed-window rate limit은 구현이 단순하지만 window boundary burst 가능성이 있습니다.
  • manual invoice generation endpoint는 검증이 쉽지만 background scheduler는 추가 과제입니다.

Limitations

  • AI provider 호출은 mock response입니다.
  • payment provider는 mock webhook contract입니다.
  • refresh token, OAuth, SSO, full IAM lifecycle은 구현 범위가 아닙니다.
  • quota check는 DB usage sum 기반이며 strict quota reservation을 주장하지 않습니다.
  • ledger는 simplified balanced-entry model이며 외부 회계 규정 충족을 주장하지 않습니다.
  • k6 scenario는 있지만 benchmark result는 Pending입니다.
프로젝트 다이어그램

아키텍처

전체 아키텍처

architecture

전체 아키텍처

Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.

Client

사용자 진입과 실시간 상태 확인

User Client

API Client

Payment Webhook

App

Application

API, 도메인 로직, 워커 처리

JWT Auth

App

TenantAccessService

App

API Key Auth

App

Mock AI Gateway

App

Usage Ingestion

App

Invoice Generation

App

Ledger Service

App

Audit Service

App

Data / Messaging

상태 저장, 캐시, 이벤트 전달

PostgreSQL

Redis Rate Limit

핵심 연결 흐름
  1. 1. User Client → JWT Auth
  2. 2. JWT Auth → TenantAccessService
  3. 3. TenantAccessService → Invoice Generation
  4. 4. TenantAccessService → API Key Auth
  5. 5. API Client → API Key Auth
  6. 6. API Key Auth → Mock AI Gateway
  7. 7. API Key Auth → Usage Ingestion
  8. 8. Mock AI Gateway → Redis Rate Limit
  9. 9. Mock AI Gateway → Usage Ingestion
  10. 10. Invoice Generation → Ledger Service
  11. 11. Payment Webhook → Ledger Service
  12. 12. TenantAccessService → Audit Service
  13. 13. API Key Auth → Audit Service
  14. 14. Invoice Generation → Audit Service
  15. 15. Payment Webhook → Audit Service
  16. 16. Usage Ingestion → PostgreSQL
  17. 17. Invoice Generation → PostgreSQL
  18. 18. Ledger Service → PostgreSQL
  19. 19. Audit Service → PostgreSQL
원본 Mermaid 보기
flowchart LR
    UserClient["User Client"]
    ApiClient["API Client"]
    subgraph App["Spring Boot Application"]
      Auth["JWT Auth"]
      Tenant["TenantAccessService"]
      ApiKey["API Key Auth"]
      Gateway["Mock AI Gateway"]
      Usage["Usage Ingestion"]
      Billing["Invoice Generation"]
      Webhook["Payment Webhook"]
      Ledger["Ledger Service"]
      Audit["Audit Service"]
    end
    Postgres[("PostgreSQL")]
    Redis[("Redis Rate Limit")]
    UserClient --> Auth
    Auth --> Tenant
    Tenant --> Billing
    Tenant --> ApiKey
    ApiClient --> ApiKey
    ApiKey --> Gateway
    ApiKey --> Usage
    Gateway --> Redis
    Gateway --> Usage
    Billing --> Ledger
    Webhook --> Ledger
    Tenant --> Audit
    ApiKey --> Audit
    Billing --> Audit
    Webhook --> Audit
    Usage --> Postgres
    Billing --> Postgres
    Ledger --> Postgres
    Audit --> Postgres

ERD

전체 ERD

erd

전체 ERD

Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.

user_accounts
bigint
id
PK
varchar
email
UK
varchar
password_hash
timestamp
created_at
organizations
bigint
id
PK
varchar
name
timestamp
created_at
organization_members
bigint
id
PK
bigint
organization_id
FK
bigint
user_id
FK
varchar
role
api_keys
bigint
id
PK
bigint
organization_id
FK
varchar
key_prefix
varchar
key_hash
varchar
status
usage_events
bigint
id
PK
bigint
organization_id
FK
varchar
idempotency_key
varchar
request_hash
varchar
metric
bigint
quantity
invoices
bigint
id
PK
bigint
organization_id
FK
varchar
billing_period
varchar
status
bigint
total_amount
varchar
currency
ledger_entries
bigint
id
PK
bigint
organization_id
FK
uuid
transaction_group_id
varchar
account
varchar
direction
bigint
amount

+ 1개 필드

audit_logs
bigint
id
PK
bigint
organization_id
FK
varchar
event_type
json
metadata
관계 요약
  1. #1 user_accounts ||--o{ organization_members · belongs
  2. #2 organizations ||--o{ organization_members · has
  3. #3 organizations ||--o{ api_keys · issues
  4. #4 organizations ||--o{ usage_events · records
  5. #5 organizations ||--o{ subscriptions · owns
  6. #6 plans ||--o{ subscriptions · selected
  7. #7 organizations ||--o{ invoices · billed
  8. #8 invoices ||--o{ invoice_items · contains
  9. #9 invoices ||--o{ payments · paid_by
  10. #10 organizations ||--o{ payment_webhook_events · receives
  11. #11 organizations ||--o{ ledger_entries · records
  12. #12 organizations ||--o{ audit_logs · audits
원본 Mermaid 보기
erDiagram
    user_accounts ||--o{ organization_members : "belongs"
    organizations ||--o{ organization_members : "has"
    organizations ||--o{ api_keys : "issues"
    organizations ||--o{ usage_events : "records"
    organizations ||--o{ subscriptions : "owns"
    plans ||--o{ subscriptions : "selected"
    organizations ||--o{ invoices : "billed"
    invoices ||--o{ invoice_items : "contains"
    invoices ||--o{ payments : "paid_by"
    organizations ||--o{ payment_webhook_events : "receives"
    organizations ||--o{ ledger_entries : "records"
    organizations ||--o{ audit_logs : "audits"

    user_accounts {
      bigint id PK
      varchar email UK
      varchar password_hash
      timestamp created_at
    }
    organizations {
      bigint id PK
      varchar name
      timestamp created_at
    }
    organization_members {
      bigint id PK
      bigint organization_id FK
      bigint user_id FK
      varchar role
    }
    api_keys {
      bigint id PK
      bigint organization_id FK
      varchar key_prefix
      varchar key_hash
      varchar status
    }
    usage_events {
      bigint id PK
      bigint organization_id FK
      varchar idempotency_key
      varchar request_hash
      varchar metric
      bigint quantity
    }
    invoices {
      bigint id PK
      bigint organization_id FK
      varchar billing_period
      varchar status
      bigint total_amount
      varchar currency
    }
    ledger_entries {
      bigint id PK
      bigint organization_id FK
      uuid transaction_group_id
      varchar account
      varchar direction
      bigint amount
      varchar currency
    }
    audit_logs {
      bigint id PK
      bigint organization_id FK
      varchar event_type
      json metadata
    }

시퀀스 다이어그램

API Key와 Usage Idempotency

sequence

API Key와 Usage Idempotency

Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.

Participants
API ClientApiKeyAuthenticationFilterMock GatewayRedis Rate LimitUsageServicePostgreSQL
  1. 1요청

    API Client → ApiKeyAuthenticationFilter

    X-API-Key

  2. 2처리

    ApiKeyAuthenticationFilter → PostgreSQL

    key_prefix lookup + hash compare

  3. 3처리

    PostgreSQL → ApiKeyAuthenticationFilter

    organization context

  4. 4요청

    API Client → Mock Gateway

    POST /v1/gateway/mock-completion

  5. 5처리

    Mock Gateway → Redis Rate Limit

    fixed-window rate limit

  6. 6이벤트

    Mock Gateway → UsageService

    record usage event

  7. 7저장

    UsageService → PostgreSQL

    insert organizationId + Idempotency-Key

  8. 8alt duplicate same payload

    control

    alt duplicate same payload

    조건: alt duplicate same payload

+ 5개 단계는 아래 상세 메시지에서 확인할 수 있습니다.

전체 메시지 상세
StepFrom → ToMessageCondition
1API Client → ApiKeyAuthenticationFilterX-API-Key-
2ApiKeyAuthenticationFilter → PostgreSQLkey_prefix lookup + hash compare-
3PostgreSQL → ApiKeyAuthenticationFilterorganization context-
4API Client → Mock GatewayPOST /v1/gateway/mock-completion-
5Mock Gateway → Redis Rate Limitfixed-window rate limit-
6Mock Gateway → UsageServicerecord usage event-
7UsageService → PostgreSQLinsert organizationId + Idempotency-Key-
8controlalt duplicate same payloadalt duplicate same payload
9PostgreSQL → UsageServiceexisting eventalt duplicate same payload
10UsageService → API Clientduplicate=truealt duplicate same payload
11controlelse same key different payloadelse same key different payload
12PostgreSQL → UsageServicerequest hash mismatchelse same key different payload
13UsageService → API Client409 Conflictelse same key different payload
원본 Mermaid 보기
sequenceDiagram
    participant Client as API Client
    participant Filter as ApiKeyAuthenticationFilter
    participant Gateway as Mock Gateway
    participant Redis as Redis Rate Limit
    participant Usage as UsageService
    participant DB as PostgreSQL
    Client->>Filter: X-API-Key
    Filter->>DB: key_prefix lookup + hash compare
    DB-->>Filter: organization context
    Client->>Gateway: POST /v1/gateway/mock-completion
    Gateway->>Redis: fixed-window rate limit
    Gateway->>Usage: record usage event
    Usage->>DB: insert organizationId + Idempotency-Key
    alt duplicate same payload
      DB-->>Usage: existing event
      Usage-->>Client: duplicate=true
    else same key different payload
      DB-->>Usage: request hash mismatch
      Usage-->>Client: 409 Conflict
    end
Invoice와 Webhook Idempotency

sequence

Invoice와 Webhook Idempotency

Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.

Participants
Admin UserBillingServicePaymentWebhookServiceLedgerServiceAuditServicePostgreSQL
  1. 1처리

    Admin User → BillingService

    generate invoice(period)

  2. 2처리

    BillingService → PostgreSQL

    unique organizationId + billingPeriod

  3. 3처리

    BillingService → LedgerService

    invoice issued entries

  4. 4처리

    LedgerService → PostgreSQL

    debit/credit entries

  5. 5처리

    BillingService → AuditService

    INVOICE_GENERATED

  6. 6이벤트

    Admin User → PaymentWebhookService

    payment.succeeded providerEventId

  7. 7이벤트

    PaymentWebhookService → PostgreSQL

    reserve providerEventId

  8. 8처리

    PaymentWebhookService → LedgerService

    payment entries

+ 2개 단계는 아래 상세 메시지에서 확인할 수 있습니다.

전체 메시지 상세
StepFrom → ToMessageCondition
1Admin User → BillingServicegenerate invoice(period)-
2BillingService → PostgreSQLunique organizationId + billingPeriod-
3BillingService → LedgerServiceinvoice issued entries-
4LedgerService → PostgreSQLdebit/credit entries-
5BillingService → AuditServiceINVOICE_GENERATED-
6Admin User → PaymentWebhookServicepayment.succeeded providerEventId-
7PaymentWebhookService → PostgreSQLreserve providerEventId-
8PaymentWebhookService → LedgerServicepayment entries-
9LedgerService → PostgreSQLbalanced entry group-
10PaymentWebhookService → AuditServicePAYMENT_WEBHOOK_PROCESSED-
원본 Mermaid 보기
sequenceDiagram
    participant Admin as Admin User
    participant Billing as BillingService
    participant Webhook as PaymentWebhookService
    participant Ledger as LedgerService
    participant Audit as AuditService
    participant DB as PostgreSQL
    Admin->>Billing: generate invoice(period)
    Billing->>DB: unique organizationId + billingPeriod
    Billing->>Ledger: invoice issued entries
    Ledger->>DB: debit/credit entries
    Billing->>Audit: INVOICE_GENERATED
    Admin->>Webhook: payment.succeeded providerEventId
    Webhook->>DB: reserve providerEventId
    Webhook->>Ledger: payment entries
    Ledger->>DB: balanced entry group
    Webhook->>Audit: PAYMENT_WEBHOOK_PROCESSED
Ledger Invariant 검증

sequence

Ledger Invariant 검증

Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.

Participants
Billing/Payment ServiceLedgerServicePostgreSQL
  1. 1처리

    Billing/Payment Service → LedgerService

    create entry group

  2. 2처리

    LedgerService → LedgerService

    positive amount check

  3. 3처리

    LedgerService → LedgerService

    single currency check

  4. 4처리

    LedgerService → LedgerService

    debit sum == credit sum

  5. 5alt valid group

    control

    alt valid group

    조건: alt valid group

  6. 6alt valid group

    LedgerService → PostgreSQL

    append ledger_entries

    조건: alt valid group

  7. 7저장

    PostgreSQL → LedgerService

    saved

    조건: alt valid group

  8. 8else invalid group

    control

    else invalid group

    조건: else invalid group

+ 1개 단계는 아래 상세 메시지에서 확인할 수 있습니다.

전체 메시지 상세
StepFrom → ToMessageCondition
1Billing/Payment Service → LedgerServicecreate entry group-
2LedgerService → LedgerServicepositive amount check-
3LedgerService → LedgerServicesingle currency check-
4LedgerService → LedgerServicedebit sum == credit sum-
5controlalt valid groupalt valid group
6LedgerService → PostgreSQLappend ledger_entriesalt valid group
7PostgreSQL → LedgerServicesavedalt valid group
8controlelse invalid groupelse invalid group
9LedgerService → Billing/Payment Servicerejectelse invalid group
원본 Mermaid 보기
sequenceDiagram
    participant Service as Billing/Payment Service
    participant Ledger as LedgerService
    participant DB as PostgreSQL
    Service->>Ledger: create entry group
    Ledger->>Ledger: positive amount check
    Ledger->>Ledger: single currency check
    Ledger->>Ledger: debit sum == credit sum
    alt valid group
      Ledger->>DB: append ledger_entries
      DB-->>Ledger: saved
    else invalid group
      Ledger-->>Service: reject
    end