본문으로 건너뛰기

빌링 서비스

이 페이지는 요금제, 구독, billing key, invoice, 결제 감사 추적, Alimtalk credit을 담당하는 플랫폼 billing 모듈 lumie-backend/modules/billing의 레퍼런스입니다.

소스 경로

Path역할
lumie-backend/modules/billing/src/main/java/com/lumie/billing/adapter/in/web/*.javaplan, billing key, payment, subscription, credit, Toss webhook receiver를 위한 공개 HTTP surface
lumie-backend/modules/billing/src/main/java/com/lumie/billing/application/service/BillingSubscribeService.java카드 등록, 구독 변경, 첫 결제를 포함한 유료 플랜 구독 흐름 orchestration
lumie-backend/modules/billing/src/main/java/com/lumie/billing/application/service/SubscriptionCommandService.java구독 lifecycle 명령, trial provisioning, upgrade, downgrade, cancellation, first-charge entrypoint
lumie-backend/modules/billing/src/main/java/com/lumie/billing/adapter/in/internal/BillingServiceAdapter.javalibs/internal-api를 통해 다른 모듈이 소비하는 프로세스 내부 billing API
lumie-backend/modules/billing/src/main/java/com/lumie/billing/adapter/in/event/TenantCreatedListener.javatenant 생성 완료 후 trial subscription을 provision하는 AFTER_COMMIT listener
lumie-backend/modules/billing/src/main/java/com/lumie/billing/adapter/in/scheduling/*.javaShedLock으로 보호되는 월간 청구 및 예약 상태 전이 작업
lumie-backend/modules/billing/src/main/java/com/lumie/billing/adapter/out/external/{TossPaymentClient,PopbillTaxInvoiceClient}.java외부 결제 및 세금계산서 adapter
lumie-backend/modules/billing/src/main/resources/db/migration/V1__create_billing_tables.sql원래 billing schema를 위한 모듈 로컬 bootstrap
lumie-backend/app/src/main/resources/db/migration/public/{V28__billing_platform_tables,V31__billing_add_missing_columns,V33__billing_keys_add_customer_key,V37__subscription_scheduled_changes,V38__create_billing_operation_locks,V39__subscription_scheduled_change_indexes}.sqlbilling table과 scheduling metadata에 대한 현재 public schema source of truth

공개 인터페이스

Endpoint목적
GET /v1/plans, GET /v1/plans/{planId}plan metadata 목록과 단일 plan 조회
GET /v1/billing/configToss client key 같은 browser-safe billing config 반환
POST /v1/billing-keys, GET /v1/billing-keys, GET /v1/billing-keys/active, DELETE /v1/billing-keys/{billingKeyId}저장된 billing key 등록, 조회, revoke
POST /v1/payments/confirm, GET /v1/payments/history, GET /v1/payments/history/{tenantSlug}일회성 Toss 결제 확정과 invoice 이력 조회
POST /v1/billing/subscribe“카드 등록 + 플랜 전환 + 첫 invoice 청구”를 한 번에 수행하는 흐름
POST /v1/subscriptions, GET /v1/subscriptions/{tenantSlug}, PATCH /v1/subscriptions/{tenantSlug}, DELETE /v1/subscriptions/{tenantSlug}, DELETE /v1/subscriptions/{tenantSlug}/cancellation, POST /v1/subscriptions/{tenantSlug}/charge구독 lifecycle 관리
GET /v1/subscriptions/{tenantSlug}/quota/{metricType}런타임 제한은 우회 중이지만 호출자에게 노출되는 quota check surface
GET /v1/alimtalk/credits, POST /v1/alimtalk/credits/recharge, PUT /v1/alimtalk/credits/auto-recharge, DELETE /v1/alimtalk/credits/auto-rechargeAlimtalk credit 잔액 및 auto-recharge 설정 조회/변경
POST /internal/webhooks/toss/billingmonolith 내부에서 Toss billing webhook을 검증하고 파싱

Toss webhook receiver를 제외한 모든 변경성 공개 billing endpoint는 Idempotency-Key를 요구합니다.

내부 인터페이스와 의존성

Surface코드 기준 실제 상태
lumie-backend/libs/internal-api/src/main/java/com/lumie/billing/api/BillingService.javasubscription 조회, plan feature, quota check, 무료 subscription provisioning을 위한 공개 프로세스 내부 계약
BillingServiceAdapter공개 계약을 구현하며, getActiveSubscription(...), consumeAlimtalkCredit(...) 같은 인터페이스 밖 helper도 포함
TenantCreatedEventtenant 모듈이 발행하고 TenantCreatedListener가 소비하여 commit 후 trial subscription을 provision
PaymentGatewayPortToss billing key 발급, 일회성 결제 확인, 청구, 취소, webhook 검증을 위한 추상화
TaxInvoicePortPopbill adapter 기반 세금계산서 발행 추상화

집계와 테이블

Aggregate참고
Plan제한과 feature를 담는 공개 plan catalog
SubscriptionACTIVE, PAUSED, PAST_DUE, CANCELLED, EXPIRED 같은 상태를 가지는 플랫폼 범위 subscription aggregate
BillingKeytenant별 저장 카드 및 billing key 상태
Invoicesubscription 청구 invoice와 결제 확인 상태
PaymentTransaction요청, 응답, webhook 캡처에 사용되는 변경 불가능한 결제 게이트웨이 audit log
AlimtalkCredittenant 범위 메시지 credit 잔액과 auto-recharge 설정
TaxInvoice세금계산서 발행 기록과 실패 metadata

런타임 흐름

tenant signup은 TenantCreatedListener를 통해 두 번째 billing 흐름을 실행합니다. tenant 모듈이 TenantCreatedEvent를 발행하고, billing이 commit 후 trial PRO subscription을 provision합니다.

계약 참고 사항

subscription aggregate에는 trial 만료, 예약 downgrade, period-end 취소, 연속 결제 실패 후 auto-pause를 포함한 실제 상태 머신이 들어 있습니다.

// lumie-backend/modules/billing/src/main/java/com/lumie/billing/adapter/in/internal/BillingServiceAdapter.java
@Override
public QuotaResult checkQuota(String tenantSlug, MetricType metricType) {
return new QuotaResult(true, 0, Long.MAX_VALUE, metricType,
"Billing integration pending - unlimited access granted");
}

quota 계약은 존재하지만, 런타임 enforcement는 현재 비활성화되어 있습니다. 호출자는 아직 quota endpoint를 실제 제한 장치로 취급하면 안 됩니다.

예시 계약

이 예시는 BillingSubscribeController, SubscribeRequest, SubscribeResponse, TossBillingWebhookController, TossBillingWebhookPayload에서 직접 가져왔습니다.

유료 플랜 구독

POST /v1/billing/subscribe
Idempotency-Key: subscribe-01
Content-Type: application/json

{
"authKey": "bln_abc",
"customerKey": "lumie-tenant-15",
"planId": "PRO"
}
HTTP/1.1 200 OK

{
"subscription": {
"id": 1,
"tenantSlug": "demo",
"planId": "PRO",
"status": "ACTIVE",
"billingKeyId": 99
},
"billingKey": {
"id": 99,
"maskedCardNumber": "1234-****-****-5678",
"cardCompany": "현대",
"status": "ACTIVE"
},
"invoice": {
"id": 500,
"invoiceNumber": "INV-15-20260528-AB",
"amount": 99000,
"vatAmount": 0
}
}

BillingSubscribeControllerTest는 외부 소비 필드인 subscription.planId, billingKey.id, invoice.amount를 검증합니다.

Toss Billing 웹훅

POST /internal/webhooks/toss/billing
Toss-Signature: t=1712345678,v1=<hmac-hex>
Content-Type: application/json

{
"eventType": "PAYMENT.STATUS_CHANGED",
"createdAt": "2026-06-14T12:34:56Z",
"data": {
"orderId": "INV-15-20260528-AB",
"paymentKey": "pay_123",
"billingKey": "bill_123",
"customerKey": "lumie-tenant-15",
"status": "DONE",
"totalAmount": 99000
}
}
HTTP/1.1 200 OK

TossBillingWebhookController는 서명을 검증하고 TossBillingWebhookPayload를 파싱한 뒤 eventType을 로그에 남기고 200을 반환하기만 합니다. dispatch TODO가 남아 있으므로, webhook 파싱 성공이 아직 billing 상태 변경을 의미하지는 않습니다.

실패, 재시도, 관측성

  • BillingSubscribeServicebilling_operation_locks를 통해 tenant당 동시 subscribe 흐름을 직렬화합니다. 경쟁 요청은 409 OPERATION_IN_PROGRESS를 받습니다.
  • Subscription.chargeAttemptFailed()는 subscription을 PAST_DUE로 옮기고, 세 번 연속 실패하면 auto-pause합니다.
  • MonthlySubscriptionChargeSchedulerSubscriptionScheduledChangeScheduler는 ShedLock 아래에서 동작하므로, 한 시점에 하나의 pod만 작업을 처리합니다.
  • TenantCreatedListener는 Spring Modulith AFTER_COMMIT listener입니다. trial provisioning이 실패하면 event publication은 미완료 상태로 남고, 재시작 시 재시도됩니다.
  • PaymentTransaction은 결제 게이트웨이 요청/응답 payload의 주요 audit trail입니다.
  • TossBillingWebhookController는 현재 서명 검증과 TossBillingWebhookPayload 파싱까지만 수행하며, dispatch TODO가 남아 있어 아직 downstream billing 변경은 일어나지 않습니다.

검증

cd lumie-backend
./gradlew :modules:billing:test
./gradlew :modules:billing:test --tests '*BillingSubscribeControllerTest'
./gradlew :modules:billing:test --tests '*TenantCreatedListenerTest'

예상 성공 신호:

  • Gradle이 BUILD SUCCESSFUL로 종료되고, BillingSubscribeControllerTest가 여전히 $.subscription.planId, $.billingKey.id, $.invoice.amount를 검증합니다.
  • TossBillingWebhookController가 성공적인 검증과 파싱 후 여전히 200 OK를 반환하고, inline TODO가 webhook dispatch가 아직 연결되지 않았음을 보여줍니다.

관련 페이지