#데이터베이스 마이그레이션 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_on에 condition: 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