권한 (Entitlement) 시스템
문서 정보
- 작성일: 2026-02-27
- 최종 업데이트: 2026-02-27
- 버전: v1.0.0
Entitlement 테이블은 사용자 1명당 1개의 레코드로 현재 멤버십 권한을 관리합니다. API 요청 시 이 테이블 하나만 조회하면 O(1)으로 권한 판단이 완료되며, 구독/체험/관리자 부여 등 어떤 경로로 권한을 받았든 동일한 방식으로 처리합니다.
목차
- 왜 Entitlement 테이블이 필요한가
- Entitlement 테이블 구조
- 권한 부여 소스
- 권한 판별 흐름
- Guard 기반 접근 제어
- Entitlement 라이프사이클
- 만료 처리 스케줄러
- 기능별 접근 권한 정리
- FAQ
왜 Entitlement 테이블이 필요한가
기존 방식의 문제
Entitlement 도입 이전에는 사용자의 PLUS 권한을 확인하려면 여러 테이블을 동시에 조회해야 했습니다.
| 문제 | 설명 |
|---|---|
| 느린 응답 | 매 요청마다 2~3개 테이블을 조회해야 함 |
| 복잡한 로직 | 구독 상태, 체험 기간, 관리자 부여를 모두 종합해서 판단 |
| 확장 어려움 | 새로운 권한 부여 방식이 생길 때마다 판단 로직 수정 필요 |
Entitlement의 해결 방식
핵심 아이디어는 간단합니다. "복잡한 판단은 권한이 변경될 때 한 번만 하고, 조회는 항상 단순하게"
| 항목 | 기존 방식 | Entitlement 방식 |
|---|---|---|
| 조회 횟수 | 2~3개 테이블 | 1개 테이블, 1건 |
| 판단 시점 | API 요청 시 매번 | 이벤트 발생 시 한 번 |
| Guard 역할 | 여러 테이블 종합 판단 | 단순 조건 비교만 |
| 새 권한 소스 추가 | Guard 로직 수정 필요 | EntitlementService만 확장 |
Entitlement 테이블 구조
사용자 1명당 정확히 1개의 레코드만 존재합니다. userId가 곧 기본 키(Primary Key)입니다.
| 필드 | 설명 | 예시 |
|---|---|---|
| userId | 사용자 ID (기본 키, User와 1:1) | 12345 |
| plan | 현재 플랜 | PLUS 또는 FREE |
| status | 권한 상태 | ACTIVE, INACTIVE, SUSPENDED |
| validFrom | 권한 시작일 | 2026-02-27T00:00:00Z |
| validUntil | 권한 종료일 | 2026-03-27T00:00:00Z |
| sourceType | 권한이 어디서 왔는지 | SUBSCRIPTION, TRIAL, ADMIN |
| sourceId | 원본 레코드 ID | 구독 ID 또는 정책 적용 ID |
하나의 사용자에게 항상 하나의 레코드만 유지됩니다. 여러 권한 소스가 경쟁할 경우 가장 강한 권한이 우선합니다.
상태(status) 의미
| 상태 | 의미 | 서비스 접근 |
|---|---|---|
| ACTIVE | 유효한 멤버십 | 허용 |
| INACTIVE | 만료 또는 미가입 | 차단 |
| SUSPENDED | 환불/부정 사용 등으로 정지 | 차단 |
만료 시 레코드 처리
레코드는 삭제되지 않고 업데이트됩니다. 만료되면 다음과 같이 변경됩니다:
| 필드 | 만료 전 | 만료 후 |
|---|---|---|
| plan | PLUS | FREE |
| status | ACTIVE | INACTIVE |
| sourceType | SUBSCRIPTION 또는 TRIAL | null |
| sourceId | 원본 ID | null |
따라서 모든 가입 사용자에게 Entitlement 레코드가 존재합니다 (회원가입 시 체험으로 자동 생성).
권한 부여 소스
PLUS 멤버 십은 세 가지 경로를 통해 부여됩니다.
| 소스 | 설명 | sourceId가 가리키는 곳 |
|---|---|---|
| SUBSCRIPTION | 유료 정기 구독 | 구독(Subscription) 레코드 |
| TRIAL | 회원가입 시 무료 체험 | 마케팅 정책 적용 레코드 |
| ADMIN | 관리자가 수동으로 부여 | 없음 (null) |
체험(TRIAL)과 구독(SUBSCRIPTION)은 모두 PLUS 멤버십이지만, 일부 기능은 구독 전용입니다. 자세한 내용은 기능별 접근 권한 정리를 참고하세요.
권한 판별 흐름
API 요청이 들어왔을 때, 권한을 판별하는 전체 흐름입니다.
구독 갱신 유예 시간 (Grace Period)
구독(SUBSCRIPTION) 사용자에게는 30분의 유예 시간이 적용됩니다. 정기 결제 갱신과 권한 체크 사이의 시간차로 인해 일시적으로 서비스가 차단되는 것을 방지하기 위한 장치입니다.
| 소스 | 유예 시간 | 이유 |
|---|---|---|
| SUBSCRIPTION | 30분 | 결제 갱신 처리 시간차 방지 |
| TRIAL | 없음 (정확히 만료) | 정해진 기간만 보장 |
| ADMIN | 없음 | 관리자가 직접 관리 |
Guard 기반 접근 제어
@RequiresPlus() 데코레이터
API 엔드포인트에 @RequiresPlus()를 붙이면 PLUS 멤버십 사용자만 접근할 수 있습니다. 권한 소스(sourceType)에 따라 세밀하게 제어할 수 있습니다.
| 사용 방식 | 의미 | 실제 사용 예시 |
|---|---|---|
@RequiresPlus() | 모든 PLUS 사용자 허용 | 웨어러블 기기 연결 |
@RequiresPlus(['SUBSCRIPTION', 'ADMIN']) | 구독자 + 관리자만 허용 | 콕 리워드, 마이콕, 리워드 전환 |
PlusGuard의 동작 원리
PlusGuard는 다음 세 가지 조건만 확인합니다:
plan이 PLUS인가?status가 ACTIVE인가?validUntil이 현재 시각 이후인가?
세 조건을 모두 통과하면 PLUS 멤버십이 유효합니다. 추가로 sourceType 제한이 있으면 허용 목록과 비교합니다.
Guard는 오직 Entitlement 테이블만 조회합니다. 다음은 Guard에서 절대 수행하지 않습니다:
- 구독 테이블 직접 조회
- 마케팅 정책 테이블 조회
- 여러 레코드를 종합해서 판단
- 결제 상태 해석
모든 복잡한 판단은 EntitlementService가 이벤트 발생 시점에 처리하고, Guard는 그 결과만 확인합 니다.
Entitlement 라이프사이클
사용자의 Entitlement는 다음과 같은 흐름으로 변화합니다.
주요 이벤트별 처리
| 이벤트 | 처리 메서드 | 결과 |
|---|---|---|
| 회원가입 | createFromTrial() | PLUS 체험 시작 |
| 구독 결제 성공 | upsertFromSubscription() | 구독 기반 PLUS로 전환 |
| 구독 갱신 | upsertFromSubscription() | validUntil 연장 |
| 체험 만료 | deactivateExpiredTrials() | FREE로 변경 |
| 구독 만료 | deactivateExpiredSubscriptions() | FREE로 변경 |
| 환불 | restoreTrialAfterRefund() | 남은 체험 복원 또는 FREE |
구독 전환 시 동작
체험 기간 중에 유료 구독을 시작하면, Entitlement가 SUBSCRIPTION으로 덮어써집니다. 이는 구독이 체험보다 더 강한 권한이기 때문입니다.
| 항목 | 변경 전 (체험) | 변경 후 (구독) |
|---|---|---|
| sourceType | TRIAL | SUBSCRIPTION |
| sourceId | 정책 적용 ID | 구독 ID |
| validUntil | 체험 종료일 | 다음 결제일 |
| validFrom | 유지 | 유지 (최초 생성 시점 보존) |
만료 처리 스케줄러
ExpireEntitlementScheduler
매일 새벽 3시(KST)에 실행되어 만료된 체험과 구독을 일괄 처리합니다.
처리 순서와 이유
| 순서 | 대상 | 조건 | 이유 |
|---|---|---|---|
| 1 | 체험 정책 적용 | expiresAt < now & ACTIVE | 원본 데이터 상태를 먼저 기록 |
| 2 | 체험 Entitlement | sourceType = TRIAL & validUntil < now | 정책 상태 변경 후 권한 비활성화 |
| 3 | 구독 Entitlement | sourceType = SUBSCRIPTION & validUntil < now - 30분 | Grace Period 적용 후 비활성화 |
로그 및 리포트
만료 처리 결과는 두 곳에 기록됩니다:
| 기록 위치 | 형식 | 용도 |
|---|---|---|
logs/entitlement/YYYY-MM-DD/ | 로그 파일 | 상세 처리 내역 보관 |
| Discord Daily Report | 요약 메시지 | 운영팀 모니터링 |
기능별 접근 권한 정리
모든 PLUS 사용자 (체험 포함)
체험(TRIAL)과 구독(SUBSCRIPTION) 모두 사용 가능한 기능입니다.
| 기능 | 설명 |
|---|---|
| 웨어러블 기기 연결 | 웨어러블 디바이스 등록 및 연동 |
구독 전용 기능
유료 구독자만 사용 가능하며, 체험 사용자는 접근할 수 없습니다.
| 기능 | 설명 | 제한 이유 |
|---|---|---|
| 콕 리워드 | 콕 시청 완료 시 리워드 적립 | 유료 전환 인센티브 |
| 마이콕 | 콕 개인 저장소 | 스토리지 비용 |
| 리워드 전환 | 리워드 → 캐쉬 전환 | 재화 정책 |
접근 제어 구조
체험은 구독의 부분집합입니다. 구독 전용 기능은 유료 결제를 통해서만 접근할 수 있습니다.
FAQ
Q: Entitlement 레코드가 없는 사용자가 있을 수 있나요?
체험 자동 적용이 도입되기 이전에 가입한 사용자는 Entitlement 레코드 가 없을 수 있습니다. 이 경우 API 응답에서 entitlement: null로 표시되며, 권한 체크에서는 비(非)PLUS로 처리됩니다.
Q: 체험 기간 중에 구독하면 체험은 어떻게 되나요?
Entitlement가 SUBSCRIPTION으로 덮어써집니다. 만약 구독을 환불하면, 남은 체험 기간이 있을 경우 자동으로 체험이 복원됩니다.
Q: 구독 만료 직후에 서비스가 바로 차단되나요?
아닙니다. 구독(SUBSCRIPTION) 사용자에게는 30분의 유예 시간(Grace Period)이 적용됩니다. 정기 결제 갱신 처리 중에 일시적으로 서비스가 차단되는 것을 방지하기 위한 장치입니다. 체험(TRIAL)에는 유예 시간이 적용되지 않습니다.