ADR-11: 보존 기간 기반 데이터 정리 서비스
| 날짜 | 작성자 | 레포지토리 |
|---|---|---|
| 2026-02-02 | @KubrickCode | worker, infra |
컨텍스트
데이터 증가 문제
Specvital 플랫폼의 시간 기반 데이터 누적:
| 데이터 유형 | 테이블 | 증가 패턴 | 보존 필요 |
|---|---|---|---|
| 분석 이력 | user_analysis_history | 커밋별 분석 | 티어별 보존 |
| 스펙 문서 | spec_documents | SpecView 생성별 | 티어별 보존 |
| 분석 | analyses | 공유 분석 레코드 | 고아 정리 |
| 사용 이벤트 | usage_events | 연산별 (ADR-13) | 감사 + 정리 |
| 할당량 예약 | quota_reservations | 동시 요청별 | 단기 (TTL) |
적극적인 정리 없이는 스토리지 비용 증가 및 쿼리 성능 저하 발생.
보존 정책 요구사항
구독 플랜별 티어 기반 보존 기간:
| 티어 | 보존 기간 | 근거 |
|---|---|---|
| Free | 30일 | 비용 통제 |
| Pro | 90일 | 표준 비즈니스 요구 |
| Pro Plus | 180일 | 확장 이력 |
| Enterprise | 무제한 (NULL) | 컴플라이언스 요구사항 |
다운그레이드 공정성 문제
과제: 사용자가 Pro에서 Free로 다운그레이드 시 Pro 티어 데이터(90일 보존)를 즉시 삭제해야 하는가?
답변: 아니오. 상위 티어 플랜에서 생성된 데이터는 원래 보존 기간 유지. 레코드 생성 시점에 보존 정책 캡처 필요 (정리 시점 조회가 아님).
아키텍처 컨텍스트
Scheduler 서비스 제거(ADR-22)로 중앙 집중식 크론 실행기 폐지. 기존 Scheduler 내 정리 태스크는 Railway Cron 배포로 전환.
결정
데이터 보존 정리를 독립 바이너리(cmd/retention-cleanup)로 구현, Railway Cron 통해 매일 UTC 03:00 트리거.
생성 시점 보존 스냅샷
레코드 생성 시 retention_days_at_creation 저장:
sql
ALTER TABLE user_analysis_history ADD COLUMN retention_days_at_creation integer;
ALTER TABLE spec_documents ADD COLUMN retention_days_at_creation integer;
-- 제약 조건: 양수 또는 NULL
CHECK ((retention_days_at_creation IS NULL) OR (retention_days_at_creation > 0))
-- 정리 쿼리용 부분 인덱스
CREATE INDEX idx_uah_retention_cleanup ON user_analysis_history (created_at)
WHERE (retention_days_at_creation IS NOT NULL);값 의미론:
| 값 | 의미 |
|---|---|
| NULL | 무제한 보존 (enterprise/레거시) |
| 양수 정수 | 삭제 대상이 되기까지의 일수 |
2단계 정리 전략
┌─────────────────────────────────────────────────────────────────┐
│ 정리 실행 순서 │
├─────────────────────────────────────────────────────────────────┤
│ Phase 1a: 만료된 user_analysis_history 삭제 │
│ WHERE created_at + retention_days < NOW() │
│ │
│ Phase 1b: 만료된 spec_documents 삭제 │
│ WHERE created_at + retention_days < NOW() │
│ │
│ Phase 2: 고아 analyses 삭제 │
│ WHERE user_analysis_history 참조 없음 │
│ AND created_at < NOW() - 1 day (유예 기간) │
└─────────────────────────────────────────────────────────────────┘바이너리 아키텍처
cmd/retention-cleanup/
├── main.go # 진입점, run-to-completion
└── (bootstrap via internal/)
src/internal/
├── domain/retention/
│ ├── errors.go # 도메인 오류
│ ├── policy.go # 만료 계산
│ └── repository.go # CleanupRepository 인터페이스
├── usecase/retention/
│ └── cleanup.go # CleanupUseCase 오케스트레이션
└── adapter/repository/postgres/
└── retention.go # PostgreSQL 구현CleanupRepository 인터페이스
go
type CleanupRepository interface {
DeleteExpiredUserAnalysisHistory(ctx context.Context, batchSize int) (DeleteResult, error)
DeleteExpiredSpecDocuments(ctx context.Context, batchSize int) (DeleteResult, error)
DeleteOrphanedAnalyses(ctx context.Context, batchSize int) (DeleteResult, error)
}
type DeleteResult struct {
DeletedCount int64
HasMore bool
}Railway 설정
json
{
"$schema": "https://railway.com/railway.schema.json",
"build": {
"builder": "DOCKERFILE",
"dockerfilePath": "infra/retention-cleanup/Dockerfile"
},
"deploy": {
"cronSchedule": "0 3 * * *",
"restartPolicyType": "NEVER"
}
}검토된 대안
옵션 A: 개별 바이너리 + Railway Cron (선택됨)
Railway Cron 통해 매일 실행되는 독립 Go 바이너리.
장점:
- 다른 워크로드와 장애 격리
- 독립적 배포 및 설정
- 24/7 실행 비용 없음 (실행당 과금)
- Railway의 스케줄링 신뢰성 활용
단점:
- 콜드 스타트 지연 (~10-30초)
- Railway 플랫폼 종속성
- 관리할 바이너리 추가
옵션 B: Scheduler 서비스 내장 (이전 방식)
중앙 집중식 Scheduler 내 크론 작업으로 정리 로직 포함.
장점:
- 단일 서비스 모니터링
- 웜 실행 (콜드 스타트 없음)
단점:
- ADR-22에 따라 Scheduler 제거됨
- 일일 작업에 24/7 실행 비용
옵션 C: PostgreSQL pg_cron 확장
데이터베이스 레벨 예약 작업.
장점:
- 애플리케이션 바이너리 불필요
- 네이티브 PostgreSQL 솔루션
단점:
- Railway Postgres에서 pg_cron 사용 불가
- SQL로 복잡한 티어 인식 로직 구현 어려움
- 애플리케이션 레벨 로깅 불가
옵션 D: 정리 시점 현재 플랜 조회
생성 시 저장 대신 정리 시 사용자의 현재 플랜 조회.
장점:
- 스키마 변경 불필요
- 항상 현재 티어 반영
단점:
- 다운그레이드 불공정: 사용자가 Free로 다운그레이드 시 Pro 데이터 즉시 삭제
- 보존 비용을 지불한 데이터 손실
결과
긍정적
| 영역 | 이점 |
|---|---|
| 스토리지 최적화 | 무제한 테이블 증가 방지 |
| 쿼리 성능 | 작은 테이블로 인덱스 효율성 유지 |
| 비용 통제 | 실행당 과금 (24/7 프로세스 없음) |
| 다운그레이드 공정성 | 기존 데이터는 원래 보존 기간 유지 |
| 컴플라이언스 준비 | 자동 데이터 생명주기 관리 |
| 감사 추적 | 실행당 삭제 건수 로깅 |
부정적
| 영역 | 트레이드오프 |
|---|---|
| 스케줄링 세분화 | 일일 최소 (Railway Cron 제한) |
| 콜드 스타트 | 실행당 10-30초 시작 오버헤드 |
| 플랫폼 종속성 | Railway에 스케줄링 종속 |
| 스키마 추가 | 쓰기 빈도 높은 테이블에 새 컬럼 추가 |
기술 참고
- 삭제 순서: Phase 1을 Phase 2 전에 실행 (외래 키 인식)
- 배치 처리: 설정 가능한 배치 크기 및 슬립 간격
- 타임아웃: Railway 실행 타임아웃 30분 설정
- 모니터링: 테이블별 삭제 건수 로깅으로 트렌드 분석
설정
DATABASE_URL=postgres://...
RETENTION_TIMEOUT=30m
RETENTION_BATCH_SIZE=1000
RETENTION_BATCH_SLEEP=100ms참고
- ADR-22: Scheduler 제거 및 Railway Cron 전환 - 상위 아키텍처 결정
- Worker ADR-08: SpecView Worker 바이너리 분리 - 바이너리 분리 패턴
- ADR-13: 과금 및 할당량 아키텍처 - 플랜의
retention_days정의 - 커밋:
7eb93aa,878d87c,5e8c05a,792f106,6e03a7f(2026-02-02)
