빌링 서비스
이 페이지는 요금제, 구독, billing key, invoice, 결제 감사 추적,
Alimtalk credit을 담당하는 플랫폼 billing 모듈
lumie-backend/modules/billing의 레퍼런스입니다.
소스 경로
| Path | 역할 |
|---|---|
lumie-backend/modules/billing/src/main/java/com/lumie/billing/adapter/in/web/*.java | plan, 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.java | libs/internal-api를 통해 다른 모듈이 소비하는 프로세스 내부 billing API |
lumie-backend/modules/billing/src/main/java/com/lumie/billing/adapter/in/event/TenantCreatedListener.java | tenant 생성 완료 후 trial subscription을 provision하는 AFTER_COMMIT listener |
lumie-backend/modules/billing/src/main/java/com/lumie/billing/adapter/in/scheduling/*.java | ShedLock으로 보호되는 월간 청구 및 예약 상태 전이 작업 |
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}.sql | billing table과 scheduling metadata에 대한 현재 public schema source of truth |
공개 인터페이스
| Endpoint | 목적 |
|---|---|
GET /v1/plans, GET /v1/plans/{planId} | plan metadata 목록과 단일 plan 조회 |
GET /v1/billing/config | Toss 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-recharge | Alimtalk credit 잔액 및 auto-recharge 설정 조회/변경 |
POST /internal/webhooks/toss/billing | monolith 내부에서 Toss billing webhook을 검증하고 파싱 |
Toss webhook receiver를 제외한 모든 변경성 공개 billing endpoint는
Idempotency-Key를 요구합니다.
내부 인터페이스와 의존성
| Surface | 코드 기준 실제 상태 |
|---|---|
lumie-backend/libs/internal-api/src/main/java/com/lumie/billing/api/BillingService.java | subscription 조회, plan feature, quota check, 무료 subscription provisioning을 위한 공개 프로세스 내부 계약 |
BillingServiceAdapter | 공개 계약을 구현하며, getActiveSubscription(...), consumeAlimtalkCredit(...) 같은 인터페이스 밖 helper도 포함 |
TenantCreatedEvent | tenant 모듈이 발행하고 TenantCreatedListener가 소비하여 commit 후 trial subscription을 provision |
PaymentGatewayPort | Toss billing key 발급, 일회성 결제 확인, 청구, 취소, webhook 검증을 위한 추상화 |
TaxInvoicePort | Popbill adapter 기반 세금계산서 발행 추상화 |
집계와 테이블
| Aggregate | 참고 |
|---|---|
Plan | 제한과 feature를 담는 공개 plan catalog |
Subscription | ACTIVE, PAUSED, PAST_DUE, CANCELLED, EXPIRED 같은 상태를 가지는 플랫폼 범위 subscription aggregate |
BillingKey | tenant별 저장 카드 및 billing key 상태 |
Invoice | subscription 청구 invoice와 결제 확인 상태 |
PaymentTransaction | 요청, 응답, webhook 캡처에 사용되는 변경 불가능한 결제 게이트웨이 audit log |
AlimtalkCredit | tenant 범위 메시지 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 상태 변경을 의미하지는 않습니다.
실패, 재시도, 관측성
BillingSubscribeService는billing_operation_locks를 통해 tenant당 동시 subscribe 흐름을 직렬화합니다. 경쟁 요청은409 OPERATION_IN_PROGRESS를 받습니다.Subscription.chargeAttemptFailed()는 subscription을PAST_DUE로 옮기고, 세 번 연속 실패하면 auto-pause합니다.MonthlySubscriptionChargeScheduler와SubscriptionScheduledChangeScheduler는 ShedLock 아래에서 동작하므로, 한 시점에 하나의 pod만 작업을 처리합니다.TenantCreatedListener는 Spring Modulith AFTER_COMMIT listener입니다. trial provisioning이 실패하면 event publication은 미완료 상태로 남고, 재시작 시 재시도됩니다.PaymentTransaction은 결제 게이트웨이 요청/응답 payload의 주요 audit trail입니다.TossBillingWebhookController는 현재 서명 검증과TossBillingWebhookPayload파싱까지만 수행하며, dispatchTODO가 남아 있어 아직 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를 반환하고, inlineTODO가 webhook dispatch가 아직 연결되지 않았음을 보여줍니다.