codingstairs
노트에듀라이프연락
⌕검색⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

연락하기

로그인 없이도 보낼 수 있어요. 답변이 필요하면 이메일을 함께 적어 주세요.

  • 익명 폼으로 의견 남기기 →
  • ✉ warragon112@gmail.com
  • 카카오톡 오픈채팅 ↗

© 2026 codingstairs

  • 노트
  • 에듀
  • 검색
  • 라이프
  • 연락
  • 약관
  • RSS
  • GitHub
에듀›모노레포 · SSOT · 계층 분리 사고›4단계

4단계

SQL = SSOT

0회 조회

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. 컬럼 추가 절차 (운영 중 테이블)

  1. 운영 DB 에 직접 ALTER:
docker exec prod-postgres psql -U ... \
  -c "ALTER TABLE posts ADD COLUMN IF NOT EXISTS subtitle TEXT;"
  1. 해당 CREATE 파일의 CREATE TABLE 블록 안에 컬럼 추가.
CREATE TABLE IF NOT EXISTS posts (
  ...
  subtitle TEXT,             -- 추가
  ...
);
  1. 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

← 3단계

폴더를 계약으로

5단계 →

점진 리팩터 · 트레이드오프