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으로 분리했습니다.
membership/RBAC 기반 organization-scoped API 접근 제어
usage request hash와 providerEventId duplicate/conflict 처리
No load test has been executed yet
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 원문 미저장을 확인했습니다.
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 처리를 검증했습니다.
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로만 설명합니다.
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. User Client → JWT Auth
- 2. JWT Auth → TenantAccessService
- 3. TenantAccessService → Invoice Generation
- 4. TenantAccessService → API Key Auth
- 5. API Client → API Key Auth
- 6. API Key Auth → Mock AI Gateway
- 7. API Key Auth → Usage Ingestion
- 8. Mock AI Gateway → Redis Rate Limit
- 9. Mock AI Gateway → Usage Ingestion
- 10. Invoice Generation → Ledger Service
- 11. Payment Webhook → Ledger Service
- 12. TenantAccessService → Audit Service
- 13. API Key Auth → Audit Service
- 14. Invoice Generation → Audit Service
- 15. Payment Webhook → Audit Service
- 16. Usage Ingestion → PostgreSQL
- 17. Invoice Generation → PostgreSQL
- 18. Ledger Service → PostgreSQL
- 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 --> PostgresERD
전체 ERD▼
erd
전체 ERD
Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.
user_accounts
- bigint
- id
- PK
- varchar
- 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 user_accounts ||--o{ organization_members · belongs
- #2 organizations ||--o{ organization_members · has
- #3 organizations ||--o{ api_keys · issues
- #4 organizations ||--o{ usage_events · records
- #5 organizations ||--o{ subscriptions · owns
- #6 plans ||--o{ subscriptions · selected
- #7 organizations ||--o{ invoices · billed
- #8 invoices ||--o{ invoice_items · contains
- #9 invoices ||--o{ payments · paid_by
- #10 organizations ||--o{ payment_webhook_events · receives
- #11 organizations ||--o{ ledger_entries · records
- #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
- 1요청
API Client → ApiKeyAuthenticationFilter
X-API-Key
- 2처리
ApiKeyAuthenticationFilter → PostgreSQL
key_prefix lookup + hash compare
- 3처리
PostgreSQL → ApiKeyAuthenticationFilter
organization context
- 4요청
API Client → Mock Gateway
POST /v1/gateway/mock-completion
- 5처리
Mock Gateway → Redis Rate Limit
fixed-window rate limit
- 6이벤트
Mock Gateway → UsageService
record usage event
- 7저장
UsageService → PostgreSQL
insert organizationId + Idempotency-Key
- 8alt duplicate same payload
control
alt duplicate same payload
조건: alt duplicate same payload
+ 5개 단계는 아래 상세 메시지에서 확인할 수 있습니다.
전체 메시지 상세
| Step | From → To | Message | Condition |
|---|---|---|---|
| 1 | API Client → ApiKeyAuthenticationFilter | X-API-Key | - |
| 2 | ApiKeyAuthenticationFilter → PostgreSQL | key_prefix lookup + hash compare | - |
| 3 | PostgreSQL → ApiKeyAuthenticationFilter | organization context | - |
| 4 | API Client → Mock Gateway | POST /v1/gateway/mock-completion | - |
| 5 | Mock Gateway → Redis Rate Limit | fixed-window rate limit | - |
| 6 | Mock Gateway → UsageService | record usage event | - |
| 7 | UsageService → PostgreSQL | insert organizationId + Idempotency-Key | - |
| 8 | control | alt duplicate same payload | alt duplicate same payload |
| 9 | PostgreSQL → UsageService | existing event | alt duplicate same payload |
| 10 | UsageService → API Client | duplicate=true | alt duplicate same payload |
| 11 | control | else same key different payload | else same key different payload |
| 12 | PostgreSQL → UsageService | request hash mismatch | else same key different payload |
| 13 | UsageService → API Client | 409 Conflict | else 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
endInvoice와 Webhook Idempotency▼
sequence
Invoice와 Webhook Idempotency
Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.
Participants
- 1처리
Admin User → BillingService
generate invoice(period)
- 2처리
BillingService → PostgreSQL
unique organizationId + billingPeriod
- 3처리
BillingService → LedgerService
invoice issued entries
- 4처리
LedgerService → PostgreSQL
debit/credit entries
- 5처리
BillingService → AuditService
INVOICE_GENERATED
- 6이벤트
Admin User → PaymentWebhookService
payment.succeeded providerEventId
- 7이벤트
PaymentWebhookService → PostgreSQL
reserve providerEventId
- 8처리
PaymentWebhookService → LedgerService
payment entries
+ 2개 단계는 아래 상세 메시지에서 확인할 수 있습니다.
전체 메시지 상세
| Step | From → To | Message | Condition |
|---|---|---|---|
| 1 | Admin User → BillingService | generate invoice(period) | - |
| 2 | BillingService → PostgreSQL | unique organizationId + billingPeriod | - |
| 3 | BillingService → LedgerService | invoice issued entries | - |
| 4 | LedgerService → PostgreSQL | debit/credit entries | - |
| 5 | BillingService → AuditService | INVOICE_GENERATED | - |
| 6 | Admin User → PaymentWebhookService | payment.succeeded providerEventId | - |
| 7 | PaymentWebhookService → PostgreSQL | reserve providerEventId | - |
| 8 | PaymentWebhookService → LedgerService | payment entries | - |
| 9 | LedgerService → PostgreSQL | balanced entry group | - |
| 10 | PaymentWebhookService → AuditService | PAYMENT_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_PROCESSEDLedger Invariant 검증▼
sequence
Ledger Invariant 검증
Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.
Participants
- 1처리
Billing/Payment Service → LedgerService
create entry group
- 2처리
LedgerService → LedgerService
positive amount check
- 3처리
LedgerService → LedgerService
single currency check
- 4처리
LedgerService → LedgerService
debit sum == credit sum
- 5alt valid group
control
alt valid group
조건: alt valid group
- 6alt valid group
LedgerService → PostgreSQL
append ledger_entries
조건: alt valid group
- 7저장
PostgreSQL → LedgerService
saved
조건: alt valid group
- 8else invalid group
control
else invalid group
조건: else invalid group
+ 1개 단계는 아래 상세 메시지에서 확인할 수 있습니다.
전체 메시지 상세
| Step | From → To | Message | Condition |
|---|---|---|---|
| 1 | Billing/Payment Service → LedgerService | create entry group | - |
| 2 | LedgerService → LedgerService | positive amount check | - |
| 3 | LedgerService → LedgerService | single currency check | - |
| 4 | LedgerService → LedgerService | debit sum == credit sum | - |
| 5 | control | alt valid group | alt valid group |
| 6 | LedgerService → PostgreSQL | append ledger_entries | alt valid group |
| 7 | PostgreSQL → LedgerService | saved | alt valid group |
| 8 | control | else invalid group | else invalid group |
| 9 | LedgerService → Billing/Payment Service | reject | else 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