PostgreSQL 스키마 변경을 Git으로 추적하기 — pg-delta가 이끄는 선언적 DB GitOps
야근하다가 프로덕션에 ALTER TABLE을 날렸는데 ACCESS EXCLUSIVE LOCK이 걸리면서 서비스가 멈췄던 경험, 있으신가요? 저는 있습니다. 그리고 그 변경이 누가, 언제, 왜 했는지 Slack DM과 Jira 티켓을 뒤져야 했던 답답함도요. 코드는 Git 히스토리에 다 남는데, 왜 DB 스키마는 어딘가 회색지대에 있는 걸까요.
이 글에서는 Supabase가 개발한 pg-delta를 중심으로, DB 스키마를 코드와 동일한 방식으로 관리하는 선언적 GitOps 워크플로우를 살펴봅니다. Flyway나 Liquibase처럼 "이 SQL을 이 순서로 실행해"라고 지시하는 방식 대신, "이 DB는 이런 상태여야 해"라고 최종 상태만 선언하면 도구가 나머지를 알아서 계산하는 방식입니다. 핵심은 현재 DB와 선언된 상태의 차이(delta)를 자동으로 계산해, SQL 작성부터 리뷰까지 코드와 동일한 프로세스를 DB에도 적용한다는 것입니다.
GitHub Actions 연동 CI 파이프라인 예시, pgschema와 Atlas를 선택할 수 있는 상황별 기준, 그리고 프로덕션에서 반드시 알아야 할 주의사항까지 솔직하게 다뤄보겠습니다.
핵심 개념
명령형 vs 선언적 — 어디서 본 것 같은 느낌
Flyway나 Liquibase 같은 전통적인 도구는 명령형(Imperative) 방식입니다. V001__create_users.sql, V002__add_email_column.sql처럼 "이 순서대로 이 SQL을 실행해"라고 지시합니다. 히스토리가 쌓이면 수십 개의 파일을 순서대로 따라가야 지금 스키마가 어떤 모양인지 파악할 수 있습니다.
선언적(Declarative) 방식은 다릅니다. schema.sql 파일 하나에 "지금 이 DB는 이런 테이블 구조여야 해"라고 최종 상태만 써두면 됩니다. 도구가 현재 DB 상태와 비교해서 필요한 DDL을 자동으로 만들어줍니다.
선언적 스키마 관리란 원하는 DB 최종 상태를 Git에 정의해두고, 도구가 현재 상태와의 차이(delta)를 계산해 마이그레이션을 자동 생성하는 방식입니다.
GitOps는 Git을 모든 변경의 시작점으로 삼는 운영 패러다임입니다. 인프라나 앱 배포에 이미 익숙한 방식인데, 이걸 DB 스키마에도 적용하는 것입니다. 스키마 변경을 코드 변경처럼 PR → Review → Merge → Apply 흐름으로 다루는 거라고 보시면 됩니다.
| 기존 방식 | GitOps 방식 |
|---|---|
| 개발자가 직접 SQL 작성 후 실행 | 선언적 스키마 파일 수정 후 PR |
| 변경 기록이 Slack·Jira에 흩어짐 | Git 커밋에 변경 이력 집중 |
| 환경별 스키마가 조금씩 달라짐 | CI가 환경 간 상태를 자동 동기화 |
| 리뷰 없이 직접 DB 접속 | 코드 리뷰와 동일한 승인 프로세스 |
스키마 드리프트(Schema Drift) 란 개발·스테이징·프로덕션 환경의 DB 스키마가 서로 달라지는 현상입니다. "내 로컬에선 됐는데요"의 DB 버전이라고 생각하시면 됩니다.
이 글에서 다루는 세 가지 도구
본격적인 예시 전에, 등장하는 도구들을 한 번 정리해두겠습니다.
| 도구 | 역할 | 적합한 환경 |
|---|---|---|
| pg-delta (Supabase) | PostgreSQL 스키마 diff 엔진. 두 상태를 비교해 올바른 DDL 자동 생성 | Supabase 프로젝트, 또는 diff 엔진만 교체하고 싶은 경우 |
| pgschema | Go 기반 CLI. migration 추적 테이블 없는 순수 선언적 워크플로우 | Supabase 없이 PostgreSQL을 직접 운영하는 팀 |
| Atlas | HCL/SQL 선언 + Kubernetes Operator + GitHub Actions 통합 | Kubernetes 환경, 또는 엔터프라이즈급 거버넌스가 필요한 팀 |
셋 중 뭘 써야 하는지 아직 감이 잘 안 오실 수 있습니다. 지금은 "pg-delta가 Supabase의 핵심 diff 엔진이고, pgschema와 Atlas는 Supabase 없이도 비슷한 선언적 워크플로우를 제공하는 별도 도구"라는 것만 알고 계시면 됩니다. 뒤에서 각각의 예시를 보면서 자연스럽게 선택 기준이 보일 겁니다.
pg-delta가 migra보다 나은 이유
pg-delta는 Supabase의 pg-toolbelt 모노레포에 포함된 핵심 패키지로, 기존에 같은 역할을 하던 Python 기반 migra를 대체합니다. 역할은 명확합니다: 두 PostgreSQL 스키마 상태를 비교해서 올바른 마이그레이션 DDL을 자동 생성하는 diff 엔진입니다.
작동 방식을 간단히 설명하면, pg-delta는 "원하는 상태"(schema.sql에 선언된 것)와 "현재 상태"(실제 DB)를 각각 파싱해 AST 수준에서 비교한 뒤, 두 상태 사이의 차이를 메우는 DDL을 도출합니다. migra가 가끔 RENAME을 DROP + CREATE로 잘못 해석하거나, ALTER SEQUENCE 같은 PostgreSQL 고유 DDL을 처리하지 못했던 문제들을 pg-delta에서 개선했습니다. migra를 쓰다가 컬럼 이름을 바꿨을 때 기존 데이터가 날아갈 뻔했다면, 아마 공감하실 겁니다.
Supabase CLI에서는 두 가지 방식으로 활성화할 수 있는데, 미묘하게 다릅니다.
# 기존 마이그레이션 파일 워크플로우를 유지하되, diff 엔진만 pg-delta로 교체
# (V001__, V002__ 파일 방식을 그대로 쓰면서 생성 품질만 올리고 싶을 때)
supabase db pull --diff-engine pg-delta
# 완전한 선언적 워크플로우로 전환
# (마이그레이션 파일 대신 schema.sql 하나를 관리하는 방식으로 바꿀 때)
supabase db pull --use-pg-delta처음 도입이라면 --diff-engine pg-delta로 기존 방식을 유지하면서 엔진만 바꿔보는 것도 좋은 시작점입니다.
실전 적용
이 섹션의 세 예시는 독립적입니다. 자신의 환경과 가장 가까운 예시부터 보셔도 됩니다.
Supabase + GitHub Actions: PR에 스키마 변경 계획 자동 게시하기
Supabase 프로젝트를 쓰고 있다면 가장 자연스러운 시작점입니다. supabase/schemas/ 디렉터리에 선언적 스키마 파일을 두고, PR이 열릴 때마다 변경 계획을 자동으로 PR 코멘트에 올려주는 워크플로우입니다.
동작하려면 저장소 Secrets에 SUPABASE_ACCESS_TOKEN과 SUPABASE_PROJECT_REF가 설정되어 있어야 합니다. GitHub 저장소 Settings → Secrets and variables → Actions에서 추가하시면 됩니다.
# .github/workflows/schema-check.yml
name: Schema Plan
on:
pull_request:
paths:
- 'supabase/schemas/**'
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Supabase CLI
uses: supabase/setup-cli@v1
- name: Link Supabase project
run: supabase link --project-ref ${{ secrets.SUPABASE_PROJECT_REF }}
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
- name: Generate schema diff
run: |
supabase db diff --use-pg-delta --linked --schema public > migration_plan.sql
- name: Comment plan on PR
uses: actions/github-script@v7
with:
script: |
const plan = require('fs').readFileSync('migration_plan.sql', 'utf8')
if (!plan.trim()) return
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## 스키마 변경 계획\n\`\`\`sql\n${plan}\n\`\`\``
})이 파이프라인의 핵심은 supabase db diff --use-pg-delta입니다. pg-delta가 현재 연결된 DB 상태와 수정된 스키마 파일을 비교해 마이그레이션 SQL을 자동 생성하고, 그 내용이 PR 코멘트로 올라옵니다.
| 단계 | 실행 내용 | 담당 |
|---|---|---|
link |
Supabase 프로젝트에 CLI 연결 | Supabase CLI |
db diff |
스키마 파일과 DB 상태 비교 | pg-delta 엔진 |
| PR 코멘트 | 변경 계획을 팀에 자동 공유 | GitHub Actions |
팀원이 PR에서 코드 리뷰와 스키마 변경 계획을 함께 검토할 수 있게 됩니다. "이 컬럼 추가가 왜 필요한가요?"가 코드 PR과 같은 공간에서 논의되는 경험은 생각보다 팀 문화를 많이 바꿔줍니다.
pgschema: Supabase 없이 순수 PostgreSQL에 선언적 워크플로우 도입하기
Supabase를 쓰지 않는 팀이라면 pgschema가 좋은 대안입니다. Go 기반 CLI로, migration 추적 테이블도 없고 별도 서버도 없이 순수하게 두 상태를 비교해서 적용합니다.
이 예시는 작성 시점(2025년 5월) 기준 정보입니다. pgschema의 지원 PostgreSQL 버전은 공식 저장소에서 확인하시는 것을 권장합니다.
# 1. Dump: 현재 DB 스키마를 SQL 파일로 추출 (최초 1회, 또는 외부 변경 반영 시)
pgschema dump --url $DATABASE_URL > schema.sql
# 2. Plan: 수정된 schema.sql과 라이브 DB를 비교해 마이그레이션 DDL 생성
pgschema plan --url $DATABASE_URL --schema schema.sql --output plan.json
# 3. Apply: 검증된 플랜을 적용
# ⚠️ --auto-approve는 CI 환경 전용. 로컬에서는 플랜 내용을 직접 확인 후 적용하는 것을 권장
pgschema apply --plan plan.json --auto-approve저도 처음엔 "plan.json이 뭔데 거기서 왜 한 번 더 거쳐?"라고 생각했는데, 여기에 fingerprint 검증이라는 안전장치가 숨어 있습니다. plan을 만든 시점의 DB 상태가 기록되어 있어서, apply 시점에 다른 팀원이 먼저 스키마를 변경했다면 충돌을 감지해 적용을 막아줍니다. 여러 팀이 동시에 같은 DB를 수정할 때 중요한 안전망입니다.
Atlas Kubernetes Operator: GitOps 파이프라인에 DB 스키마까지 포함하기
이 예시는 Kubernetes와 ArgoCD를 이미 운영 중인 팀을 위한 내용입니다. K8s 환경이 아니라면 이 섹션은 가볍게 훑어보셔도 됩니다.
Kubernetes 환경에서 ArgoCD를 쓴다면 Atlas Operator가 가장 네이티브한 통합을 제공합니다. 스키마를 쿠버네티스 리소스로 선언하면 ArgoCD가 Git 상태와 실제 DB를 지속적으로 동기화합니다.
DB 연결 정보는 반드시 Kubernetes Secret으로 분리해서 참조하는 것을 권장합니다. 연결 문자열을 YAML에 직접 하드코딩하면 Git에 자격 증명이 노출될 수 있습니다.
# db-credentials-secret.yaml (별도 Secret 리소스로 관리)
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
stringData:
# 프로덕션에서는 반드시 sslmode=require 또는 verify-full 사용
url: "postgres://user:pass@postgres:5432/app?sslmode=require"# atlas-schema.yaml
apiVersion: db.atlasgo.io/v1alpha1
kind: AtlasSchema
metadata:
name: app-schema
spec:
credentials:
secretKeyRef:
name: db-credentials
key: url
schema:
sql: |
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL
);
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(id),
title TEXT NOT NULL
);이 파일들을 Git에 커밋하면 ArgoCD가 감지해서 Atlas Operator를 통해 DB에 반영합니다. 인프라(쿠버네티스 리소스)와 스키마(DB 상태)를 같은 GitOps 파이프라인으로 관리할 수 있게 되는 것이죠.
장단점 분석
솔직히 말하면, 도입 초기에 가장 아팠던 건 롤백이었습니다. Git revert를 하면 코드는 되돌아오는데, 이미 삭제된 컬럼의 데이터는 돌아오지 않습니다. 이 부분을 모르고 들어갔다가 당황하는 경우가 많으니, 장점만큼 단점도 제대로 보고 시작하시면 좋겠습니다.
장점
| 항목 | 내용 |
|---|---|
| 감사 추적 | 누가 언제 왜 스키마를 변경했는지 커밋 메시지로 기록. 컴플라이언스 대응에 가장 직접적인 효과 |
| 버전 관리 | 모든 스키마 변경이 Git 커밋에 기록되어 히스토리 추적 가능 |
| 코드 리뷰 통합 | DB 변경도 PR에서 팀 리뷰를 받는 동일한 프로세스 적용 가능 |
| 자동화 | CI에서 마이그레이션 계획 자동 생성, SQL을 직접 작성하지 않아도 됨 |
| 환경 일관성 | 개발·스테이징·프로덕션 간 스키마 드리프트를 시스템 수준에서 방지 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 롤백의 어려움 | DB는 상태를 가져 단순 Git revert로 데이터까지 되돌릴 수 없음 | 스키마 적용 전 백업 스냅샷 + 별도 롤백 스크립트 준비 |
| 파괴적 변경 위험 | DROP TABLE, DROP COLUMN이 자동 생성 DDL에 포함될 수 있음 |
Atlas Schema Rules 등으로 CI 단계에서 파괴적 변경 차단 규칙 설정 |
| 동시 변경 충돌 | 여러 팀이 같은 스키마를 동시 수정 시 충돌 발생 가능 | pgschema fingerprint 검증, Atlas lint 기능 활용 |
| 대형 테이블 잠금 | ADD COLUMN NOT NULL DEFAULT 등 일부 DDL이 테이블 전체 잠금 유발 |
프로덕션 적용 전 lock_timeout 설정, 무중단 패턴 별도 검토 |
| 초기 전환 복잡도 | 기존 imperative 방식에서 전환 시 히스토리 통합 작업 필요 | 신규 프로젝트부터 적용하거나, 전환 시점 스키마를 베이스라인으로 선언 |
ACCESS EXCLUSIVE LOCK 이란 PostgreSQL이 일부 DDL 실행 시 테이블 전체를 잠그는 가장 강력한 잠금입니다. 이 잠금이 걸리면 해당 테이블에 대한 읽기/쓰기가 모두 대기 상태가 됩니다. 자동 생성된 마이그레이션에도 이 잠금을 유발하는 구문이 포함될 수 있으므로, 프로덕션 적용 전 반드시 확인이 필요합니다.
실무에서 가장 흔한 실수
-
파괴적 변경 감지 없이 자동 apply 연결: CI 파이프라인에
plan단계 없이 바로apply를 연결하면DROP COLUMN이 실수로 실행될 수 있습니다. plan을 PR에 노출하고 수동 승인 후 apply하는 구조를 권장합니다. -
롤백 계획 없이 프로덕션 배포: GitOps 파이프라인이 있다고 해서 DB 롤백이 자동으로 해결되는 건 아닙니다. 컬럼 삭제 같은 변경은 Git revert만으로 데이터가 돌아오지 않으니, 스키마 적용 전 백업 스냅샷 전략을 함께 준비해두시면 좋습니다.
-
기존 Flyway/Liquibase 파일과 혼용: 선언적 방식으로 전환 중에 기존 마이그레이션 파일도 함께 실행하면 충돌이 발생합니다. 전환 시점을 명확히 정하고, 그 이후부터는 선언적 방식만 사용하는 것이 좋습니다. 전환 시점의 스키마 상태를 베이스라인으로 dump해서 시작점으로 삼으면 깔끔하게 이어갈 수 있습니다.
마치며
DB 스키마를 Git으로 관리한다는 건, 팀 전체가 스키마 변경을 코드와 동일한 신뢰도로 다룰 수 있게 된다는 의미입니다. pg-delta가 Supabase 환경에서 이 흐름의 핵심 diff 엔진 역할을 하고, pgschema와 Atlas는 각자의 환경에 맞게 같은 패러다임을 확장합니다.
지금 바로 시작해볼 수 있는 3단계:
-
현재 DB 스냅샷 추출: Supabase를 사용 중이라면
supabase db pull --use-pg-delta --schema-only로, Supabase 없이 PostgreSQL을 직접 운영하고 있다면pgschema dump --url $DATABASE_URL > schema.sql로 현재 DB의 스키마 상태를 파일로 추출하는 것부터 시작해볼 수 있습니다. -
PR 코멘트 파이프라인 연결: 위 Supabase 예시의 GitHub Actions 워크플로우를
.github/workflows/schema-check.yml로 복사해 저장소에 추가해보시면 좋습니다. PR을 열 때마다 변경될 스키마가 SQL로 자동 코멘트되는 경험을 바로 확인하실 수 있습니다. -
파괴적 변경 감지 규칙 추가: Atlas를 사용한다면 Schema Rules 기능으로
DROP TABLE,DROP COLUMN이 포함된 마이그레이션을 CI 단계에서 자동 차단하도록 설정해볼 수 있습니다. 이 하나만 있어도 실수로 인한 사고를 크게 줄일 수 있습니다.
참고 자료
- pg-toolbelt | Supabase GitHub
- Declarative Database Schemas | Supabase 공식 문서
- pgschema GitOps 워크플로우 | pgschema.com
- pgschema | GitHub
- GitOps for Databases Part 1 | Atlas Blog
- Schema Rules: Guarding Your Database with Atlas | Atlas Blog
- The Hard Truth about GitOps and DB Rollbacks | Atlas Blog
- pg-schema-diff | Stripe GitHub
- pgschema GitHub Actions Example | GitHub
- Manage PostgreSQL Clusters with GitOps using CloudNativePG and ArgoCD | Proventa