Skip to content

ADR-11: 보존 기간 기반 데이터 정리 서비스

🇺🇸 English Version

날짜작성자레포지토리
2026-02-02@KubrickCodeworker, infra

컨텍스트

데이터 증가 문제

Specvital 플랫폼의 시간 기반 데이터 누적:

데이터 유형테이블증가 패턴보존 필요
분석 이력user_analysis_history커밋별 분석티어별 보존
스펙 문서spec_documentsSpecView 생성별티어별 보존
분석analyses공유 분석 레코드고아 정리
사용 이벤트usage_events연산별 (ADR-13)감사 + 정리
할당량 예약quota_reservations동시 요청별단기 (TTL)

적극적인 정리 없이는 스토리지 비용 증가 및 쿼리 성능 저하 발생.

보존 정책 요구사항

구독 플랜별 티어 기반 보존 기간:

티어보존 기간근거
Free30일비용 통제
Pro90일표준 비즈니스 요구
Pro Plus180일확장 이력
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

참고

Open-source test coverage insights