콘텐츠로 이동

Migration Strategy

Umbra 의 DB 스키마 마이그레이션은 Atlas 선언형 방식으로 관리한다. db/schema/ 가 desired state source of truth 이며, Atlas 가 마이그레이션을 자동 생성한다. 이 문서는 실무 절차, drift detection, 위험한 마이그레이션의 수동 승인 게이트를 정리한다.

Why this approach

Atlas 는 선언형 스키마를 입력 받아 현재 DB 와의 diff 를 자동 생성한다 (ADR-0006). 이 방식이 주는 이점:

  • Source of truth 단일화db/schema/*.sql 만 보면 "이 시스템이 어떤 상태를 원하는가" 가 명확
  • Migration 자동 생성 — 수동 작성 오류 방지
  • Drift detection — CI 가 "선언 스키마 ≠ 실제 DB" 를 감지
  • Neon branching 과 통합 — PR 별 DB 브랜치에 마이그레이션 자동 적용 검증

Repository layout

db/
├─ schema/                  # Source of truth (declarative DDL)
│  ├─ 00_schemas.sql        # CREATE SCHEMA ...
│  ├─ identity.sql          # identity schema 의 모든 테이블
│  ├─ guild.sql
│  ├─ member.sql
│  ├─ licensing.sql
│  ├─ billing.sql
│  ├─ recovery.sql
│  ├─ notification.sql
│  ├─ audit.sql
│  ├─ webhook.sql
│  └─ events.sql
├─ migrations/              # Auto-generated by Atlas
│  ├─ 20260401000000_init.sql
│  ├─ 20260415123045_add_guild_config.sql
│  └─ atlas.sum             # checksum (integrity)
├─ queries/                 # sqlc source
│  ├─ identity/
│  ├─ guild/
│  └─ ...
├─ atlas.hcl                # Atlas 설정
└─ sqlc.yaml                # sqlc 설정

Atlas config

db/atlas.hcl:

env "local" {
  src = "file://schema"
  url = "postgres://localhost:5432/umbra_dev?sslmode=disable"
  dev = "docker://postgres/16/dev"
  migration {
    dir = "file://migrations"
  }
}

env "preview" {
  src = "file://schema"
  url = env.PREVIEW_DATABASE_URL  # Neon PR 브랜치
  dev = "docker://postgres/16/dev"
  migration {
    dir = "file://migrations"
  }
}

env "prod" {
  src = "file://schema"
  url = env.PROD_DATABASE_URL
  dev = "docker://postgres/16/dev"
  migration {
    dir = "file://migrations"
  }
}

Standard workflow

1. 스키마 변경 (declarative)

개발자는 db/schema/{domain}.sql 을 수정한다.

-- db/schema/licensing.sql
ALTER TABLE licensing.licenses
ADD COLUMN suspended_reason TEXT;

아니다. 선언형이라 변경이 아니라 최종 상태를 적는다:

-- db/schema/licensing.sql
CREATE TABLE licensing.licenses (
    id                 UUID PRIMARY KEY,
    guild_id           UUID NOT NULL REFERENCES guild.guilds(id),
    plan_id            UUID NOT NULL REFERENCES licensing.plans(id),
    status             TEXT NOT NULL CHECK (status IN ('active', 'suspended', 'canceled')),
    granted_at         TIMESTAMPTZ NOT NULL,
    expires_at         TIMESTAMPTZ,
    suspended_at       TIMESTAMPTZ,
    suspended_reason   TEXT,  -- 이 줄 추가
    canceled_at        TIMESTAMPTZ,
    created_at         TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at         TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

2. Migration 자동 생성

atlas migrate diff --env local add_suspended_reason

Atlas 가 현재 DB 와 선언 스키마를 비교해 필요한 마이그레이션 SQL 을 db/migrations/20260415_add_suspended_reason.sql 로 생성.

3. 로컬 적용

atlas migrate apply --env local

4. Commit

  • db/schema/licensing.sql (수정됨)
  • db/migrations/20260415_add_suspended_reason.sql (생성됨)
  • db/migrations/atlas.sum (체크섬 갱신)

세 파일 모두 PR 에 포함.

5. CI 검증

GitHub Actions 가 PR 마다:

  • Neon PR 브랜치 DB 에 마이그레이션 적용
  • atlas migrate lint 로 위험 변경 감지
  • atlas schema diff 로 drift 재확인 (선언 스키마와 적용 결과 일치 확인)
  • drift 있으면 PR 실패

6. Merge → Production apply

main merge 시 GitHub Actions 가 production DB 에 적용:

atlas migrate apply --env prod

7. 자동 생성 파일과 함께 Commit

changed files:
  db/schema/licensing.sql
  db/migrations/20260415_add_suspended_reason.sql
  db/migrations/atlas.sum

  # sqlc 생성 파일 (새 컬럼 추가 시)
  engine/licensing/adapter/persistence/sqlc/models.go
  engine/licensing/adapter/persistence/sqlc/queries.sql.go

PR review checklist

Check Responsibility
선언 스키마 파일 변경이 의도와 일치하는가 Author
자동 생성된 migration SQL 이 예상과 일치하는가 Reviewer
Destructive 변경 포함 여부 Reviewer
CI 의 Atlas lint 통과 CI
Neon PR 브랜치에서 적용 성공 CI
sqlc 재생성 commit 여부 Author

Destructive migrations

위험한 변경 은 수동 승인 게이트.

분류

Change Risk Required gate
ADD COLUMN (nullable) Low Auto
ADD COLUMN (NOT NULL with DEFAULT) Low Auto
ADD COLUMN (NOT NULL no DEFAULT) High Manual approval
DROP COLUMN High Manual approval
ALTER COLUMN TYPE Medium~High Manual review
DROP TABLE Critical Manual approval + owner sign-off
ALTER CONSTRAINT Medium Manual review
RENAME High Multi-step migration 권장
Large UPDATE/BACKFILL High 별도 script, 본 마이그레이션과 분리

Gate 설정

GitHub Actions approval 또는 Atlas Cloud 의 policy 적용:

lint {
  destructive {
    error = true  # DROP 은 에러로 처리
  }
  data_depend {
    error = true  # 데이터 의존 변경 경고
  }
}

Multi-step rename 패턴

컬럼 이름 변경은 단일 마이그레이션으로 하지 않는다.

  1. Step 1 — 새 이름의 컬럼 추가 + 데이터 복사
  2. Step 2 — 애플리케이션 코드를 새 컬럼 사용하도록 배포
  3. Step 3 — 기존 컬럼 삭제 (다음 PR)

이 과정은 단일 PR 로 합쳐서는 안 된다. 배포 순서 깨질 위험.

Backfill scripts

데이터 이관이 필요한 경우 마이그레이션 SQL 과 분리. Atlas 마이그레이션은 schema 만, backfill 은 별도 Go script (cmd/backfill/).

db/migrations/20260415_add_col.sql      # ADD COLUMN nullable
cmd/backfill/20260415_populate_col.go   # 데이터 채우기
db/migrations/20260501_set_not_null.sql # NOT NULL 제약 추가 (다음 PR)

3-step 배포:

  1. Schema 변경 1 (nullable)
  2. Backfill 실행
  3. Schema 변경 2 (NOT NULL)

Neon branching integration

Neon 의 DB branching 을 활용.

PR branch

PR 생성 시 GitHub Actions 가:

  1. Neon 에 PR 번호로 branch 생성 (pr-123)
  2. main branch 의 snapshot 복제
  3. 해당 branch 에 마이그레이션 적용
  4. CI 테스트 실행
  5. 결과를 PR 코멘트로 게시

Schema drift detection

atlas schema inspect --url "$PREVIEW_DATABASE_URL" > actual.hcl
atlas schema inspect --url "file://schema" > desired.hcl
diff actual.hcl desired.hcl

일치하지 않으면 CI 실패.

Merge 후 정리

PR merge 시 Neon PR branch 는 자동 삭제 (cron 또는 webhook).

Rollback strategy

Atlas 는 down migration 을 기본 생성하지 않는다. 롤백은 다음 중 하나:

Option 1: New forward migration

가장 안전. 문제를 복구하는 새 마이그레이션 작성:

-- 20260502_revert_add_column.sql
ALTER TABLE licensing.licenses DROP COLUMN suspended_reason;

Option 2: Neon PITR (point-in-time recovery)

Neon 이 제공하는 PITR 로 특정 시점으로 복구. 장점: 빠름. 단점: 복구 시점 이후 데이터 손실.

Production 롤백은 반드시 팀 논의 후. 데이터 손실 risk 분석 필수.

Seed data

초기 데이터 (Plan 마스터 등) 는 migration 에 포함:

-- db/migrations/20260401000001_seed_plans.sql
INSERT INTO licensing.plans (id, code, name, ...) VALUES
  ('018f...', 'FREE', ...),
  ('018f...', 'PRO', ...),
  ('018f...', 'ENTERPRISE', ...)
ON CONFLICT (code) DO NOTHING;

Plan 가격 변경은 별도 migration 으로 UPDATE.

Dev environment setup

신규 개발자의 로컬 환경:

# Docker PostgreSQL 시작
docker compose up -d postgres

# Schema 적용
atlas migrate apply --env local

# Seed data 확인
psql $LOCAL_DATABASE_URL -c "SELECT code FROM licensing.plans;"

Production apply order

Fly.io 배포 파이프라인에서:

  1. Migration 먼저 적용
  2. 성공 시 애플리케이션 배포
  3. 실패 시 전체 배포 중단

이유: 새 컬럼을 참조하는 코드가 먼저 배포되면 런타임 에러.

Backwards-compatible migration 원칙:

  • 새 컬럼은 항상 nullable 또는 DEFAULT 로 추가 (기존 코드가 모른 채 돌아감)
  • 기존 컬럼 제거는 코드에서 사용 중단 후 다음 PR 에서

Tooling summary

Tool Purpose
Atlas CLI Migration 생성 및 적용
atlas migrate diff 선언 스키마와 DB 비교
atlas migrate apply 마이그레이션 실행
atlas migrate lint 위험 변경 감지
atlas schema inspect 현재 DB schema dump
Neon branching PR 별 DB 격리
sqlc 마이그레이션 후 Go 코드 재생성

See also

  • adr/0006-migration-atlas.md — Atlas 선택 근거
  • adr/0020-postgres-schema-per-domain.md — schema 분리
  • guides/database-conventions.md — SQL 작성 컨벤션
  • data/schema-overview.md — 전체 schema
  • Atlas Documentation