사용자의 구독 상태를 표현하는 핵심 도메인. FREE / STANDARD / PREMIUM 세 티어를 지원하며,
각 멤버십은 30일 단위로 시작·만료되고 결제(Payment) 또는 쿠폰(Coupon)에 묶여 발급된다.
유료 구독은 매일 오전 9시 스케줄러가 만료 건을 자동 갱신하며, 갱신 실패 시 isSubscriptionPaused
플래그로 일시정지된다.
sequenceDiagram
autonumber
participant U as 사용자
participant FE as 프론트엔드
participant API as Backend (AccountController)
participant SVC as AccountService
participant DB as MySQL
U->>FE: 폰 인증 완료 → 회원가입
FE->>API: 회원가입 흐름 (Auth 페이지의 verifyAndCreateAccount 단계)
API->>SVC: account.initFreeMembership()
SVC->>DB: INSERT membership (FREE, startDate=오늘, endDate=오늘+30)
Note over DB: 계정엔 항상 최소 1개 membership 존재해야 함 (findCurrentMembership null 시 UI 깨짐)
API-->>FE: 200 OK
sequenceDiagram
autonumber
participant U as 사용자
participant FE as 프론트엔드
participant API as Backend (AccountController)
participant SVC as AccountService
participant DB as MySQL
participant TOSS as Toss Payments
U->>FE: STANDARD/PREMIUM 결제 완료
FE->>API: POST /accounts/payments/confirm { orderId, paymentKey, amount }
API->>SVC: confirmPayment(...)
SVC->>TOSS: POST /v1/payments/confirm
TOSS-->>SVC: paymentKey, method, approvedAt
SVC->>DB: payment.status=DONE
SVC->>DB: account.initMembershipByPayment(payment, null)
Note over DB: addMembership() 내부에서 현재 FREE면 endDate를 어제로 당기고 현재 유료면 새 멤버십 startDate를 maxAfterMembership.endDate+1로 연속 배치
API-->>FE: { membershipType, startDate, endDate }
sequenceDiagram
autonumber
participant CRON as @Scheduled cron(0 0 9 * * ?)
participant SCHED as SubscriptionRenewalScheduler
participant SVC as BillingService
participant DB as MySQL
participant TOSS as Toss Billing API
CRON->>SCHED: processRenewals()
SCHED->>DB: findExpiringTodayWithActiveBilling(today)
Note over DB: endDate=오늘 AND isSubscriptionWithdrawal IS NULL AND PaymentMethod ACTIVATED + billingKey 존재
loop 만료 건마다
SCHED->>SVC: chargeBilling(accountId)
SVC->>TOSS: POST /v1/billing/{billingKey}
alt 결제 성공
TOSS-->>SVC: paymentKey
SVC->>DB: 새 Payment(DONE) + 새 Membership 생성
else 결제 실패
TOSS-->>SVC: 4xx/5xx
SVC->>DB: membership.isSubscriptionPaused = true
SVC->>DB: paymentMethod.deactivate(reason)
end
end
Flyway 미사용: 신규 컬럼(is_subscription_paused, is_subscription_withdrawal 등) 추가 시 마이그레이션은 외부에서 수동 적용 필요 (infra/sql/2026-05-11-prod-init.sql 패턴)
만료 후 grace period 없음: 자동 갱신 실패 시 즉시 isSubscriptionPaused=true로 전환. 사용자에게 재시도 안내·재충전 유예 기간 미구현
멤버십 시작일 보정 로직의 복잡성: Account.addMembership()이 현재 멤버십 종류에 따라 endDate를 어제로 당기거나 새 멤버십 startDate를 미래로 미는 분기 — 테스트(changeMembershipWithdrawalAvailableForTest)에서 11번 반복으로 미래 메모리십을 쌓는 코드가 있을 정도로 엣지 케이스가 많음
테스트 전용 메서드가 production 서비스 클래스에 혼재 (switchTierForTest, injectCycleUsageForTest, grantTokenPackForTest): profile gate 없이 노출 — TestAccountController로 분리되어 있다고 가정하지만 AccountService가 직접 호출 가능
멤버십 해지 시 환불 미연동: Membership.withdrawal()은 자동 갱신만 차단. 잔여 일수에 비례한 환불은 별도 cancelTossPayment 흐름으로 수동 처리