Skip to content

ADR-24: 구독 기간 일할 계산

🇺🇸 English Version

날짜작성자레포지토리
2026-01-16@specvitalweb

컨텍스트

달력 월 기반 빌링 기간은 월말에 가입한 사용자에게 불공정한 쿼터 할당 발생. 예를 들어, 1월 29일에 가입한 사용자는 전체 월 쿼터를 받지만 2월 1일에 리셋되어 실제 사용 기간 3일 동안 2개월 분량의 쿼터 획득.

이 문제는 빌링 아키텍처의 결과로 식별되었으며 공정한 기간 계산 전략 필요.

문제 시나리오

가입 날짜달력 리셋실제 사용 일수받은 쿼터
1월 1일2월 1일31일1개월
1월 29일2월 1일3일1개월

29일에 가입한 사용자가 1일에 가입한 사용자와 동일한 쿼터를 받음에도 불구하고 사용 기간은 10%에 불과.

ADR-13과의 관계

이 ADR은 ADR-13: 빌링 및 쿼터 아키텍처를 확장. ADR-13은 "가입일 기준 롤링 기간"을 선택된 전략으로 식별했으나 구현 세부사항은 미결정. ADR-13은 또한 "프로레이션 복잡성: 중간 기간 플랜 변경 시 쿼터 조정 필요"를 부정적 결과로 언급하며 향후 작업으로 남김.

결정

각 사용자의 가입 타임스탬프 기반 롤링 기간 계산 구현.

각 사용자의 빌링 기간은 실제 가입 날짜에서 시작하여 정확히 1개월 후 종료, 가입 시점에 관계없이 모든 사용자가 전체 월 쿼터 수령 보장.

구현

go
// src/backend/modules/subscription/usecase/assign_default_plan.go
now := time.Now().UTC()
periodStart := now                                       // 가입 타임스탬프
periodEnd := now.AddDate(0, 1, 0).Add(-time.Nanosecond)  // 정확히 1개월 후

핵심 설계 결정

결정근거
UTC 정규화타임존 관련 엣지 케이스 방지
AddDate(0, 1, 0)Go의 달력 월 시맨틱 사용 (2월 28일, 윤년 올바르게 처리)
나노초 감산SQL 범위 쿼리의 배타적 상한 보장
사용자별 기간 추적user_subscriptions 테이블에 기간 경계 저장

사용량 쿼리 패턴

sql
SELECT COALESCE(SUM(quota_amount), 0)::bigint AS total
FROM usage_events
WHERE user_id = $1
    AND event_type = $2
    AND created_at >= $3    -- current_period_start (포함)
    AND created_at < $4     -- current_period_end (배타)

고려한 옵션

옵션 A: 가입일 기준 롤링 기간 (선택됨)

사용자의 실제 가입 타임스탬프에서 시작하여 정확히 1개월 후 종료.

장점단점
가입 시점 무관하게 모든 사용자에게 공정사용자마다 다른 갱신 날짜
예측 가능한 갱신 날짜 (사용자 특정)배치 보고 복잡화
월말 가입 페널티 없음중앙집중 처리 창 없음
월말 가입으로 시스템 악용 불가

옵션 B: 달력 월 기간

모든 사용자에 대해 항상 월 1일부터 말일까지 기간 적용.

장점단점
단순 구현월말 가입자에게 불공정 (최대 97% 가치 손실)
쉬운 배치 처리악용 가능성 (28일 가입, 1일 리셋)
코호트 분석 정렬불공정성에 대한 사용자 불만

기각: 사용자 공정성 트레이드오프 수용 불가.

옵션 C: 프로레이트된 달력 월

달력 월 정렬을 유지하되 첫 달 부분 쿼터를 비례 배분.

장점단점
달력 정렬 유지복잡한 부분 계산
수학적으로 공정사용자에게 설명 어려움
부분 쿼터 UX 마찰

기각: 복잡성이 달력 정렬 이점 초과.

옵션 D: 고정 날짜 앵커 (1일 또는 15일)

예측 가능한 배치 창을 위해 가입일을 가장 가까운 앵커 날짜로 반올림.

장점단점
예측 가능한 배치 처리 창최대 14일 불공정
더 단순한 보고임의적 컷오프가 불공정하게 느껴짐
사용자가 가치 극대화를 위해 가입 지연 가능

기각: 임의적 불공정성이 사용자 불만족 유발.

결과

긍정적

영역이점
사용자 공정성가입 시점 무관하게 모든 사용자가 정확히 1개월 쿼터 수령
예측 가능한 갱신사용자가 정확한 갱신 날짜 인지 (UI에 특정 달력 날짜로 표시)
악용 방지즉시 리셋을 위한 월말 가입으로 시스템 악용 불가
명확한 기대UI에 "1월 16일에 리셋"으로 표시, 모호한 "월간" 대신
ADR-13 정렬상위 ADR에 문서화된 선택 전략 이행

부정적

영역트레이드오프완화책
배치 창 없음사용자마다 다른 날짜에 리셋현재 규모에서 수용 가능
인보이스 복잡성결제 연동 시 사용자별 빌링 날짜 필요결제 처리 필요 시 향후 고려
월 길이 변동1월 31일이 2월 28일로 롤 (3일 손실)Go의 AddDate가 올바르게 처리; 수용 가능한 변동
중간 기간 변경플랜 업/다운그레이드 시 프로레이션 로직 필요향후 작업으로 명시적 연기
보고 복잡성롤링 기간이 코호트 분석 복잡화사용자 공정성 우선으로 수용

처리된 엣지 케이스

엣지 케이스처리 방법
월 경계 (1월 31일 → 2월 28일)Go AddDate 달력 시맨틱
윤년 (2월 29일)Go 표준 라이브러리가 올바르게 처리
타임존 변동저장 레이어에서 UTC 정규화
경계 정밀도나노초 정밀도 배타적 상한

연기된 결정

ADR-13에 언급된 결과에 따라 다음 중간 기간 시나리오는 미구현 상태:

시나리오제안된 접근법상태
중간 기간 업그레이드비례 차이 추가: (newQuota - oldQuota) * remainingDays / totalDays향후 작업
중간 기간 다운그레이드옵션: 미사용분 이월 또는 새 한도로 리셋향후 작업
플랜 만료무료 티어로 그레이스풀 디그레이데이션향후 작업

참고 자료

Open-source test coverage insights