Skip to content

ADR-13: 빌링 및 쿼터 아키텍처

🇺🇸 English Version

날짜작성자리포지토리
2026-01-18@KubrickCodeweb, worker, infra

배경

수익화 요구사항

Specvital 테스트 분석 플랫폼의 수익화를 위한 빌링 및 쿼터 시스템 필요. 4개 구독 티어: free, pro, pro_plus, enterprise.

핵심 요구사항:

요구사항설명
사용량 추적SpecView 및 Analysis 작업의 정확한 추적
캐시 인식 빌링캐시 히트는 쿼터 미소비
감사 준수리소스 삭제 시에도 사용 기록 보존
공정한 쿼터 기간가입일과 관계없이 전체 구독 가치 제공
티어 차별화유료 사용자에게 더 빠른 처리 제공
엔터프라이즈 무제한최상위 티어의 효과적인 무제한 사용

제약사항

제약사항영향
PostgreSQL 백엔드River 큐가 PostgreSQL 사용 (ADR-04); 솔루션 통합 필수
캐시 우선 모델SpecView는 캐시된 결과를 무료로 제공; 미스만 쿼터 소비
익명 접근플랫폼에서 속도 제한이 있는 익명 탐색 허용
다중 리포지토리솔루션이 web, worker, infra 리포지토리에 걸쳐 적용

결정

이벤트 기반 사용량 추적, 롤링 쿼터 기간, 티어별 큐 분리 우선순위화 채택.

1. 이벤트 기반 사용량 추적

작업 완료 시점에 usage_events 테이블에 사용량 이벤트 기록:

sql
table usage_events {
  id: uuid
  user_id -> users
  event_type: specview | analysis
  analysis_id -> analyses? (ON DELETE SET NULL)
  document_id -> spec_documents? (ON DELETE SET NULL)
  quota_amount: int
  created_at: timestamptz
}

핵심 특성:

  • 성공적 완료 시에만 이벤트 기록
  • 캐시 히트는 이벤트 미생성
  • ON DELETE SET NULL로 감사 추적 보존
  • 효율적 쿼터 조회를 위한 월별 집계 인덱스
  • quota_amount에 SpecView의 테스트 케이스 수 저장

2. 구독 플랜 아키텍처

NULL이 무제한을 나타내는 4단계 구조:

sql
table subscription_plans {
  tier: free | pro | pro_plus | enterprise
  monthly_price: int?
  specview_monthly_limit: int?
  analysis_monthly_limit: int?
  retention_days: int?
}
sql
table user_subscriptions {
  user_id -> users
  plan_id -> subscription_plans (ON DELETE RESTRICT)
  status: active | canceled | expired
  current_period_start: timestamptz
  current_period_end: timestamptz
}

제약조건:

  • 부분 유니크 인덱스로 사용자당 하나의 활성 구독 보장
  • 사용자 가입 시 무료 플랜 자동 할당
  • 롤링 기간: 활성화 날짜로부터 정확히 1개월

3. 티어 기반 큐 우선순위화

서비스당 3개 큐와 티어 기반 라우팅:

구독자목적
_priorityPro, Pro Plus, Enterprise유료 사용자 빠른 처리
_defaultFree표준 처리
_scheduledSystem예약 재분석

서비스별 큐 이름:

Analysis Service:
  analysis_priority   → Pro/Enterprise 사용자
  analysis_default    → Free 티어 사용자
  analysis_scheduled  → 스케줄러/크론 작업

SpecView Service:
  specview_priority   → Pro/Enterprise 사용자
  specview_default    → Free 티어 사용자
  specview_scheduled  → 스케줄러/크론 작업

큐 선택 로직:

go
// common/queue/selector.go (web repository)
func SelectQueue(baseQueue string, tier PlanTier, isScheduled bool) string {
    if isScheduled {
        return baseQueue + SuffixScheduled  // "_scheduled"
    }
    switch tier {
    case PlanTierPro, PlanTierProPlus, PlanTierEnterprise:
        return baseQueue + SuffixPriority   // "_priority"
    default:
        return baseQueue + SuffixDefault    // "_default"
    }
}

요청 흐름:

Handler → TierLookup → UseCase → QueueService → SelectQueue
   │           │           │           │              │
   │     GetUserTier()     │     Enqueue()      큐 이름 계산
   │           │           │           │              │
   └───────────┴───────────┴───────────┴──────────────┘

Graceful Degradation:

  • 빈 userID 또는 nil tierLookup → _default로 라우팅
  • 티어 조회 시 데이터베이스 오류 → 경고 로그, _default로 라우팅
  • 구독 레코드 없음 → 빈 티어로 처리, _default로 라우팅

워커 할당 (환경 변수로 설정 가능):

Analyzer:
  ANALYZER_QUEUE_PRIORITY_WORKERS=5   (기본값)
  ANALYZER_QUEUE_DEFAULT_WORKERS=3    (기본값)
  ANALYZER_QUEUE_SCHEDULED_WORKERS=2  (기본값)

SpecGen:
  SPECGEN_QUEUE_PRIORITY_WORKERS=3    (기본값)
  SPECGEN_QUEUE_DEFAULT_WORKERS=2     (기본값)
  SPECGEN_QUEUE_SCHEDULED_WORKERS=1   (기본값)

4. 익명 사용자 속도 제한

고정 윈도우 방식의 IP 기반 인메모리 속도 제한:

  • IP당 분당 10개 요청
  • analyze API의 익명 사용자에만 적용
  • 인메모리 저장 (외부 의존성 없음)

검토한 옵션

A. 사용량 추적 전략

옵션결론
완료 시 이벤트 기반 (선택)감사 친화적, 캐시 정렬, 실패 안전
실시간 차감경쟁 조건, 복잡한 환불 로직
외부 미터링 (Stripe/Orb)외부 의존성, 지연, 대규모 비용

선택 근거:

이벤트 기반 추적은 기존 PostgreSQL 중심 아키텍처와 정렬되며 캐시 미스만 쿼터를 소비하는 캐시 우선 모델에 자연스럽게 적합.

B. 쿼터 기간 전략

옵션결론
가입 시점 롤링 기간 (선택)모든 사용자에게 공정, 예측 가능한 갱신
캘린더 월월말 가입자에게 불공정
커스텀 빌링 사이클최대 복잡성, 지원 부담

선택 근거:

롤링 기간은 사용자 공정성 우선. 28일에 가입하는 사용자도 캘린더 리셋까지 3일이 아닌 전체 1개월 쿼터 수령.

C. 큐 우선순위 전략

옵션결론
티어별 큐 분리 (선택)명확한 격리, SLA 친화적, 모니터링 용이
단일 큐 + 우선순위 필드우선순위 역전 위험, 복잡한 쿼리
전용 워커 풀최고 인프라 비용, 용량 공유 불가

선택 근거:

분리된 큐는 공유 워커 풀을 통한 효율성을 허용하면서 더 깔끔한 SLA 보장 제공.

크로스 서비스 아키텍처

┌─────────────────────────────────────────────────────────────────────┐
│                      빌링 & 쿼터 흐름                               │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ┌─────────────┐     ┌─────────────────────┐     ┌──────────────┐  │
│  │   Web API   │────▶│  Quota Check API    │◀───▶│  PostgreSQL  │  │
│  │  (Go + Chi) │     │  POST /usage/check  │     │              │  │
│  └──────┬──────┘     │  GET /usage/current │     │  테이블:     │  │
│         │            └─────────────────────┘     │  - users     │  │
│         │                                        │  - subscript │  │
│         ▼                                        │    ion_plans │  │
│  ┌──────────────┐    ┌─────────────────────┐    │  - user_sub  │  │
│  │ 큐 선택      │───▶│  River Queue        │    │    scriptions│  │
│  │ (티어 기반)  │    │  - :priority        │    │  - usage_    │  │
│  └──────────────┘    │  - :default         │    │    events    │  │
│                      │  - :scheduled       │    └──────────────┘  │
│                      └──────────┬──────────┘                       │
│                                 │                                  │
│                                 ▼                                  │
│  ┌─────────────────────────────────────────────────────────────┐  │
│  │                     Worker Service                           │  │
│  │  ┌───────────────┐     ┌───────────────┐                    │  │
│  │  │   Analyzer    │     │  SpecView Gen │                    │  │
│  │  │   Worker      │     │    Worker     │                    │  │
│  │  └───────┬───────┘     └───────┬───────┘                    │  │
│  │          │                     │                             │  │
│  │          ▼                     ▼                             │  │
│  │  usage_event 기록       usage_event 기록                    │  │
│  │  (type: analysis)      (type: specview)                     │  │
│  │                        (캐시 미스 시에만)                   │  │
│  └─────────────────────────────────────────────────────────────┘  │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

결과

긍정적

영역이점
빌링 정확성성공적이고 캐시되지 않은 작업만 쿼터 소비
감사 준수SET NULL로 완전한 사용 기록 보존
사용자 공정성롤링 기간으로 전체 구독 가치 보장
유료 티어 경험큐 분리로 처리 우선순위 보장
운영 유연성환경 변수로 워커 할당 조정 가능
엔터프라이즈 단순성NULL 값으로 무제한 깔끔하게 표현

부정적

영역트레이드오프
쿼터 가시성사용자가 제출 시가 아닌 작업 완료 후 사용량 업데이트 확인
리포팅 복잡성롤링 기간이 코호트 분석 복잡화
스토리지 증가이벤트 기반 추적으로 시간 경과에 따른 레코드 축적
큐 모니터링서비스당 3개 큐로 옵저버빌리티 설정 증가
비례 배분 복잡성주기 중간 플랜 변경 시 쿼터 조정 필요

기술적 시사점

측면시사점
데이터베이스 스키마월별 집계 인덱스가 있는 usage_events 테이블
쿼리 패턴인덱싱된 created_at에 대한 집계 쿼리로 월별 사용량 조회
워커 설정환경 변수로 워커-큐 비율 제어
속도 제한IP 기반 인메모리 제한으로 익명 사용자 처리 (분당 10개 요청)
플랜 전환부분 유니크 인덱스로 사용자당 단일 활성 구독 보장

참조

Open-source test coverage insights