#데이터베이스 마이그레이션 Cookbook

버전: 0.36.0 | 업데이트: 2026-03-20 | 적용 대상: ranvier-runtime 0.36+ | 카테고리: 쿡북


#개요

Ranvier는 마이그레이션 프레임워크를 내장하지 않으며, 데이터베이스 스키마 관리를 생태계 도구에 위임합니다. 이 Cookbook에서는 sqlx 마이그레이션과 refinery 통합, Docker Compose 시작 순서, CI/CD 파이프라인 설정 패턴을 소개합니다.


#1. 마이그레이션 도구 선택

도구 접근 방식 장점 단점
sqlx-cli SQL 파일 + CLI 컴파일 시 쿼리 검증, 비동기 네이티브 sqlx-cli 설치 필요
refinery Rust 임베디드 애플리케이션 바이너리에서 실행, 외부 도구 불필요 컴파일 시 쿼리 체크 없음
diesel_migrations Rust 임베디드 성숙한 생태계, CLI 제공 동기 전용, diesel 의존성

권장: sqlx를 사용하는 프로젝트(Ranvier의 주요 DB 크레이트)에서는 sqlx-cli를 사용하세요. 외부 도구 없이 자체 포함된 마이그레이션이 필요하면 refinery를 사용하세요.


#2. sqlx 마이그레이션 설정

#디렉터리 구조

my-app/
├── Cargo.toml
├── migrations/
│   ├── 20260101_000001_create_users.sql
│   ├── 20260101_000002_create_orders.sql
│   └── 20260115_000001_add_order_status.sql
└── src/
    └── main.rs

#마이그레이션 파일

-- migrations/20260101_000001_create_users.sql

CREATE TABLE IF NOT EXISTS users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email TEXT NOT NULL UNIQUE,
    name TEXT NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_users_email ON users(email);

#마이그레이션 실행

# sqlx-cli 설치
cargo install sqlx-cli --no-default-features --features postgres

# 보류 중인 마이그레이션 실행
DATABASE_URL=postgres://user:pass@localhost:5432/mydb sqlx migrate run

# 마이그레이션 상태 확인
sqlx migrate info

#애플리케이션 시작 시 임베디드 실행

use sqlx::PgPool;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let pool = PgPool::connect(&std::env::var("DATABASE_URL")?).await?;

    // HTTP 서버 시작 전에 마이그레이션 실행
    sqlx::migrate!("./migrations")
        .run(&pool)
        .await?;

    tracing::info!("마이그레이션 완료");

    Ranvier::http::<AppResources>()
        .bind("0.0.0.0:8080")
        .get("/api/users", list_users)
        .run(AppResources { db: pool })
        .await?;

    Ok(())
}

#3. refinery 마이그레이션 설정

#Cargo.toml

[dependencies]
refinery = { version = "0.8", features = ["tokio-postgres"] }
tokio-postgres = "0.7"

#마이그레이션 파일

migrations/
├── V1__create_users.sql
├── V2__create_orders.sql
└── V3__add_audit_tables.sql

#애플리케이션에 임베디드

use refinery::config::Config;

mod embedded {
    use refinery::embed_migrations;
    embed_migrations!("./migrations");
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let mut config = Config::from_env_var("DATABASE_URL")?;
    embedded::migrations::runner().run_async(&mut config).await?;
    tracing::info!("refinery 마이그레이션 완료");

    // Ranvier 서버 시작...
    Ok(())
}

#4. Ranvier 영속성 테이블

Saga 체크포인팅을 위해 PostgresPersistenceStore를 사용한다면, 마이그레이션 세트에 해당 스키마를 포함하세요:

-- migrations/20260101_000003_ranvier_persistence.sql

CREATE TABLE IF NOT EXISTS ranvier_checkpoints (
    pipeline_id TEXT NOT NULL,
    execution_id UUID NOT NULL,
    node_index INT NOT NULL,
    state_json JSONB NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    PRIMARY KEY (execution_id, node_index)
);

CREATE INDEX idx_checkpoints_pipeline ON ranvier_checkpoints(pipeline_id);

PostgresAuditSink를 사용한다면 다음을 포함하세요:

-- migrations/20260101_000004_ranvier_audit.sql

CREATE TABLE IF NOT EXISTS ranvier_audit_events (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    pipeline_id TEXT NOT NULL,
    execution_id UUID NOT NULL,
    node_label TEXT NOT NULL,
    event_type TEXT NOT NULL,
    payload JSONB,
    hash TEXT NOT NULL,
    prev_hash TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_audit_pipeline ON ranvier_audit_events(pipeline_id, execution_id);
CREATE INDEX idx_audit_created ON ranvier_audit_events(created_at);

#5. Docker Compose 시작 순서

애플리케이션 시작 전에 데이터베이스가 준비되어 있는지 확인하세요:

# docker/compose.yml
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: secret
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d myapp"]
      interval: 5s
      timeout: 3s
      retries: 10
    volumes:
      - pgdata:/var/lib/postgresql/data

  app:
    build:
      context: .
      dockerfile: Containerfile
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      DATABASE_URL: postgres://appuser:secret@postgres:5432/myapp
      RUST_LOG: info

volumes:
  pgdata:

핵심: depends_oncondition: service_healthy를 사용하면 앱 컨테이너가 PostgreSQL 시작뿐 아니라 실제 준비 완료까지 대기합니다.

#애플리케이션 레벨 재시도

헬스체크가 있더라도 애플리케이션에 연결 재시도를 추가하세요:

async fn connect_with_retry(url: &str, max_attempts: u32) -> PgPool {
    for attempt in 1..=max_attempts {
        match PgPool::connect(url).await {
            Ok(pool) => return pool,
            Err(e) => {
                tracing::warn!("DB 연결 시도 {}/{}: {}", attempt, max_attempts, e);
                tokio::time::sleep(std::time::Duration::from_secs(2)).await;
            }
        }
    }
    panic!("{} 시도 후 데이터베이스 연결 실패", max_attempts);
}

#6. CI/CD 파이프라인

#GitHub Actions 예제

name: 마이그레이션과 함께 테스트
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_DB: testdb
          POSTGRES_USER: testuser
          POSTGRES_PASSWORD: testpass
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    env:
      DATABASE_URL: postgres://testuser:testpass@localhost:5432/testdb

    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable

      - name: sqlx-cli 설치
        run: cargo install sqlx-cli --no-default-features --features postgres

      - name: 마이그레이션 실행
        run: sqlx migrate run

      - name: 테스트 실행
        run: cargo test --workspace

#7. 마이그레이션 모범 사례

#명명 규칙

모호하지 않은 순서를 위해 타임스탬프 접두사를 사용하세요:

YYYYMMDD_NNNNNN_description.sql
20260320_000001_create_users.sql
20260320_000002_create_orders.sql

#멱등적 마이그레이션

IF NOT EXISTS / IF EXISTS 가드를 사용하세요:

CREATE TABLE IF NOT EXISTS users ( ... );
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
ALTER TABLE orders ADD COLUMN IF NOT EXISTS status TEXT DEFAULT 'pending';

#애플리케이션과 Ranvier 스키마 분리

Ranvier 인프라 테이블(체크포인트, 감사)은 전용 스키마나 ranvier_ 접두사를 사용하여 네임스페이스 충돌을 방지하세요:

CREATE SCHEMA IF NOT EXISTS ranvier;
CREATE TABLE ranvier.checkpoints ( ... );
CREATE TABLE ranvier.audit_events ( ... );

#기존 마이그레이션 수정 금지

마이그레이션이 커밋되면 불변으로 취급하세요. 이전 마이그레이션이 생성한 테이블을 변경하더라도 새 마이그레이션을 만드세요.


#관련 문서

  • 영속성 운영 런북 -- 체크포인트와 복구
  • 배포 가이드 -- Docker 및 Kubernetes 배포
  • Saga 보상 Cookbook -- 영속성과 함께하는 Saga