콘텐츠로 이동

ADR-0025: Snapshot JSONB Storage

스냅샷은 PostgreSQL JSONB 컬럼에 전체 길드 상태를 단일 문서로 저장한다. 정규화된 관계형 저장이 아닌 문서 저장이다.

Status

Accepted

  • Decided at — 2026-04-13
  • Decided by — Pablo

Context

스냅샷은 특정 시점의 길드 구조 전체(역할 N개, 채널 M개, 권한 오버라이드 K개 등)를 저장한다. 저장 옵션:

  1. 정규화 관계형snapshot, snapshot_roles, snapshot_channels, snapshot_overrides 테이블로 분해
  2. JSONB 단일 문서snapshots.payload JSONB 에 전체 구조 저장
  3. 외부 object store — S3/R2 에 JSON 파일, DB 에는 메타데이터만

현실 관찰:

  • 스냅샷은 쓰기 1번 / 읽기 ~N번 패턴 (작성 후 복구 또는 조회 시만 읽음)
  • 스냅샷 내부 개별 요소를 쿼리할 일이 거의 없음 (전체를 한 번에 읽음)
  • 스냅샷 크기는 대형 길드라도 1MB 수준 (JSONB 에 충분)
  • 복구 시 원자성 필요 (부분 로드 금지)

Decision

스냅샷을 PostgreSQL JSONB 컬럼 에 저장한다.

CREATE TABLE recovery.snapshots (
    id           UUID PRIMARY KEY,
    guild_id     UUID NOT NULL REFERENCES guild.guilds(id),
    payload      JSONB NOT NULL,
    payload_size INTEGER NOT NULL,
    trigger      TEXT NOT NULL,       -- 'manual' | 'scheduled' | 'antinuke' | 'pre_restore'
    created_by   UUID REFERENCES identity.users(id),
    created_at   TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX snapshots_guild_created ON recovery.snapshots(guild_id, created_at DESC);

Payload schema

{
  "version": 1,
  "snapshot_at": "2026-04-13T00:00:00Z",
  "guild": {
    "name": "My Guild",
    "icon_hash": "...",
    "afk_channel_id": "...",
    "afk_timeout": 300
  },
  "roles": [
    {
      "discord_id": "...",
      "name": "Admin",
      "color": 0xFF0000,
      "permissions": "...",
      "position": 1,
      "mentionable": true
    }
  ],
  "channels": [
    {
      "discord_id": "...",
      "name": "general",
      "type": 0,
      "parent_id": "...",
      "position": 0,
      "topic": "...",
      "permission_overrides": [...]
    }
  ]
}

Payload 형식은 version 필드로 버전 관리. 새 필드는 향후 확장 가능.

선택 근거:

  • 원자적 읽기/쓰기 — 단일 INSERT 로 전체 스냅샷 저장, 단일 SELECT 로 전체 로드. 부분 저장 불가능
  • 구현 단순 — 정규화 테이블 수 개보다 관리 용이
  • 복구 워크플로우 자연스러움 — Temporal Activity 가 payload 전체를 한 번에 로드 후 처리
  • PostgreSQL JSONB 성능 — 수 MB 이하 문서의 읽기/쓰기가 충분히 빠름
  • 스키마 진화version 필드로 하위 호환 관리

Consequences

Positive

  • 저장/조회 구조 단순 (1 테이블)
  • 스냅샷 원자성 보장 (트랜잭션 경계 = INSERT 하나)
  • 백업/마이그레이션 용이 (단일 컬럼)
  • 복구 워크플로우가 payload 전체를 일관되게 받음

Negative

  • 스냅샷 내부 개별 요소 쿼리 불가 (예: "이 역할이 언제 만들어졌는가" 질문)
  • JSONB 크기가 매우 크면 (10MB+) 쓰기 성능 저하 가능
  • Schema drift 감지 어려움 (타입 체크는 애플리케이션 레이어)

Neutral

  • 개별 요소 분석이 필요해지면 audit.events 같은 별도 저장소 활용
  • TOAST (PostgreSQL 대용량 컬럼 저장) 에 의존 — 자동 압축됨

Retention policy

Plan 별 보관 정책:

  • Free — 스냅샷 없음 (Recovery 미제공)
  • Pro — 수동 스냅샷 최대 1개 + 일일 자동 스냅샷 7일 보관
  • Enterprise — 수동 스냅샷 최대 3개 + 일일 자동 스냅샷 30일 보관

초과분 자동 삭제 (rolling retention):

  • 매일 새벽 asynq cron 이 Plan 초과 스냅샷 삭제
  • AntiNuke 로 생성된 스냅샷은 Plan 한도 외 추가 보관 (7일, 플랜 무관)
  • Pre-restore 스냅샷은 24시간 보관

Size limit

  • 스냅샷 크기 한도: 5MB (현실적 초대형 길드도 여유)
  • 한도 초과 시 스냅샷 생성 실패 + 사용자 알림 (도움 요청 유도)

Versioning

Payload schema 는 version 필드로 관리한다.

  • v1 — MVP
  • v2 이상 — 호환 가능한 확장은 옵션 필드 추가로
  • Breaking change — 새 버전 지정 + 읽기 시점 migration 로직 또는 재스냅샷 유도

복구 코드는 읽을 때 version 체크 후 처리. 미지원 버전은 에러.

Alternatives considered

Alternative 1: 정규화 관계형 저장

Pros

  • SQL 로 개별 요소 쿼리 가능
  • Foreign Key 로 데이터 무결성

Cons

  • 스냅샷 저장 시 여러 테이블 INSERT → 트랜잭션 복잡
  • 복구 시 여러 쿼리로 payload 재구성 → JOIN 비용
  • Schema 변경 시 migration 부담 ↑

Why rejected — 스냅샷의 읽기 패턴(전체 로드)에 정규화 이점이 없음. 복잡도만 증가.

Alternative 2: S3/R2 외부 object store

Pros

  • 대용량 파일 저장에 특화
  • DB 부하 감소

Cons

  • 인프라 추가 (S3 또는 R2 관리)
  • 트랜잭션 경계 분리 (스냅샷 저장과 DB 레코드 동기화 문제)
  • MVP 에 과함

Why rejected — 5MB 이하 문서는 JSONB 가 충분. Phase 2+ 에서 크기가 폭증하면 검토.

Alternative 3: JSONB + 추가 정규화 테이블 (hybrid)

Pros

  • 전체 저장 + 개별 쿼리 가능

Cons

  • 같은 데이터 중복 저장 → drift 가능
  • 복잡도 ↑ without 명확한 이점

Why rejected — drift 위험이 큼.

Compliance

  • 스냅샷 payload 는 recovery.snapshots.payload 에만 저장
  • payload 직접 참조는 engine/recovery/subdomain/snapshot/ 에서만
  • 새 payload 필드 추가 시 version 유지, breaking change 시 증가
  • 5MB 한도 체크는 애플리케이션 레이어에서 강제
  • Retention cron 은 asynq job 으로 매일 실행

Revisit triggers

  • 스냅샷 크기가 5MB 를 초과하는 사례가 빈번하면 chunking 또는 S3 저장 검토
  • 개별 요소 쿼리 요구가 반복되면 이벤트 sourcing 또는 보조 테이블 추가 검토
  • PostgreSQL JSONB 성능이 병목이 되면 전용 document DB 재평가

References

  • ADR-0024 — Restore 가 payload 를 읽음
  • ADR-0026 — AntiNuke 자동 스냅샷
  • ADR-0004 — Neon PostgreSQL (JSONB 지원)