4단계
SQL = SSOT
25 분
SQL = SSOT
DB 스키마의 진실은 무엇인가? ORM 모델? 운영 DB? 마이그레이션 파일? 선택에 따라 작업 흐름이 달라집니다. "SQL 파일이 SSOT" 는 단순하고 재현 가능한 선택.
1. 세 가지 선택지
| 방식 | 진실 | 특징 |
|---|---|---|
| ORM-first | Entity 클래스 | Rails · Django. synchronize:true 로 자동 |
| Migration-first | 순차 마이그레이션 파일 (V1__init.sql · V2__add_column.sql) |
Flyway · Liquibase. 역사 추적 |
| Declarative SQL-first | CREATE TABLE IF NOT EXISTS 파일 (선언적) |
재현 가능 · ALTER 는 수동 |
2. warragon 의 선택 — Declarative SQL-first
-- admin/sql/codingstairs/002_create_posts.sql
CREATE TABLE IF NOT EXISTS public.posts (
id BIGSERIAL PRIMARY KEY,
slug TEXT NOT NULL,
category_slug TEXT NULL,
language CHAR(2) NOT NULL DEFAULT 'ko',
content_kind VARCHAR(10) NOT NULL DEFAULT 'note'
CHECK (content_kind IN ('note', 'blog', 'edu')),
title TEXT NOT NULL,
content_md TEXT NOT NULL DEFAULT '',
published BOOLEAN NOT NULL DEFAULT FALSE,
published_at TIMESTAMPTZ NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_posts_published
ON posts (language, content_kind, published, published_at DESC);
IF NOT EXISTS 로 멱등. 파일 그대로 실행하면 항상 같은 결과.
3. 원칙
CREATE TABLE IF NOT EXISTS만CREATE INDEX IF NOT EXISTS만DROP TABLE금지 (데이터 손실)- 별도
ALTER.sql파일 영구 보관 금지
4. 컬럼 추가 절차 (운영 중 테이블)
- 운영 DB 에 직접 ALTER:
docker exec prod-postgres psql -U ... \
-c "ALTER TABLE posts ADD COLUMN IF NOT EXISTS subtitle TEXT;"
- 해당 CREATE 파일의
CREATE TABLE블록 안에 컬럼 추가.
CREATE TABLE IF NOT EXISTS posts (
...
subtitle TEXT, -- 추가
...
);
ALTER TABLE ...를 파일에 남기지 않음. CREATE 가 신규 DB 에 컬럼 포함해 바로 생성.
5. 예외 — Forward-reference FK
-- A 가 B 보다 먼저 생성되지만 A → B FK 가 필요
CREATE TABLE A ( ..., b_id BIGINT ); -- FK 없이 컬럼만
CREATE TABLE B ( id BIGSERIAL PRIMARY KEY );
DO $
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'a_b_fk') THEN
ALTER TABLE A ADD CONSTRAINT a_b_fk FOREIGN KEY (b_id) REFERENCES B(id);
END IF;
END $;
이 경우만 DO $ 블록 허용.
6. 왜 SQL-first 인가
- 재현 가능 — 파일 실행 = 동일 결과
- 리뷰 쉬움 — Git diff 로 스키마 변경 명확
- ORM 독립 — TypeORM · Prisma · raw SQL 자유 선택
- 여러 언어 앱 공유 — Java + Python + Node 가 같은 스키마
7. 왜 ORM-first 가 아닌가 (프로젝트 선택)
- ORM 마이그레이션 = 블랙박스 변환 (엔티티 → SQL)
- 여러 앱이 공유할 때 해석 차이
- 수동 DB 검사 (
\d 테이블) 결과가 엔티티와 다를 때 디버깅 난해
단점: 선언적 SQL 파일은 "이력" 보존 안 됨 (언제 어떤 컬럼 추가됐는지 git log 로만 추적). 대부분 프로젝트에서 이건 문제 아님.
8. 왜 Flyway 같은 Migration-first 가 아닌가
- 매우 좋은 방식이지만 운영 DB 스키마 = 마이그 누적 결과
- 컬럼 하나 확인하려면 여러 파일 추적
- 재설치는 마이그 전부 순차 재실행 → 느림
- 프로젝트 규모가 크고 히스토리가 중요하면 적합
warragon 은 "현재 스키마 한눈에 + 재설치 빠름" 이 우선.
9. 스키마 변경 4곳 동시 갱신
SQL 만 바꾸고 다른 곳 안 바꾸면 drift.
1. admin/sql/codingstairs/00N.sql (SSOT)
2. codingstairs-seed.ts (시드)
3. frontend/codingstairs/src/types/cms.ts (공개 사이트 타입)
4. frontend/admin/src/app/.../types.ts (관리자 타입)
PR 리뷰에서 4 곳 다 바뀌었는지 확인. 체크리스트로 고정.
10. 시드 데이터
시드는 코드 (*-seed.ts) 로 · DB 에는 멱등 INSERT.
async function seedCategories() {
if (await isTableEmpty("categories")) return;
for (const c of CATEGORIES_SEED) {
await query(
`INSERT INTO categories (...) VALUES (...)
ON CONFLICT (slug, language, content_kind) DO NOTHING`,
[...]
);
}
}
여러 번 실행해도 같은 결과.
11. 자주 걸리는 자리
- ALTER 파일 영구 보관 — SSOT 의미 퇴색
- CREATE 와 운영 DB 불일치 (drift) — psql 만 실행 · 파일 미갱신 → 신규 DB 에 컬럼 누락
- UNIQUE 인덱스 추가 후 기존 데이터 충돌 — 사전 정리 필요
- FK 순서 실수 — forward reference 예외 패턴 적용
하고픈 말
SSOT 선택은 "팀이 한 곳만 믿을 수 있다" 는 약속의 물리적 구현. 선언적 SQL 은 그 약속을 가장 단순한 파일로 표현.
Next
- 05-progressive-refactor