Drizzle ORM 마이그레이션 운영

Drizzle 스키마 정의 방식

Drizzle ORM은 TypeScript 코드 자체가 스키마 정의이자 마이그레이션의 원본(source of truth)이다. drizzle-kit이 스키마 파일을 분석해 SQL 마이그레이션을 자동 생성한다. Prisma의 .prisma 파일이나 TypeORM의 데코레이터와 달리, Drizzle은 순수 TypeScript 함수로 테이블을 선언한다:

// src/db/schema/users.ts
import { pgTable, serial, varchar, timestamp, boolean, index } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  name: varchar('name', { length: 100 }).notNull(),
  isActive: boolean('is_active').default(true).notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
}, (table) => ({
  emailIdx: index('idx_users_email').on(table.email),
}));

테이블 정의가 곧 타입이므로, 별도의 인터페이스 선언 없이 typeof users.$inferSelect로 조회 타입을, typeof users.$inferInsert로 삽입 타입을 자동 추론할 수 있다.

관계(Relations) 선언

Drizzle은 쿼리용 관계를 별도로 선언한다. DB 외래키와 ORM 관계를 분리해 유연성을 확보한다:

// src/db/schema/posts.ts
import { pgTable, serial, varchar, text, integer, timestamp } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
import { users } from './users';

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: varchar('title', { length: 300 }).notNull(),
  content: text('content'),
  authorId: integer('author_id').references(() => users.id, {
    onDelete: 'cascade',      // DB 레벨 외래키
  }).notNull(),
  publishedAt: timestamp('published_at'),
});

// ORM 레벨 관계 (쿼리 빌더용)
export const postsRelations = relations(posts, ({ one, many }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
  comments: many(comments),
}));

export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

drizzle-kit 설정

마이그레이션 도구인 drizzle-kit의 설정 파일:

// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  schema: './src/db/schema/*',       // 스키마 파일 경로 (glob 지원)
  out: './drizzle/migrations',        // 마이그레이션 출력 디렉토리
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
  verbose: true,                      // 상세 출력
  strict: true,                       // 위험 변경 시 확인 요청
});

strict: true는 컬럼 삭제, 테이블 삭제 같은 파괴적 변경에 대해 확인을 요청한다. 프로덕션 환경에서는 반드시 활성화해야 한다.

마이그레이션 생성: generate

스키마를 변경한 뒤 마이그레이션 SQL을 자동 생성한다:

# 마이그레이션 생성
npx drizzle-kit generate

# 출력:
# drizzle/migrations/
# ├── 0000_initial.sql
# ├── 0001_add_posts_table.sql
# ├── meta/
# │   ├── 0000_snapshot.json    ← 각 시점의 스키마 스냅샷
# │   ├── 0001_snapshot.json
# │   └── _journal.json         ← 마이그레이션 이력

생성된 SQL 파일은 순수 SQL이다. ORM에 종속되지 않으므로 DBA가 직접 검토하거나, CI/CD 파이프라인에서 psql로 실행할 수도 있다:

-- 0001_add_posts_table.sql (자동 생성된 마이그레이션)
CREATE TABLE IF NOT EXISTS "posts" (
  "id" serial PRIMARY KEY NOT NULL,
  "title" varchar(300) NOT NULL,
  "content" text,
  "author_id" integer NOT NULL,
  "published_at" timestamp,
  CONSTRAINT "posts_author_id_users_id_fk"
    FOREIGN KEY ("author_id") REFERENCES "users"("id")
    ON DELETE cascade
);

마이그레이션 실행: migrate

애플리케이션 코드에서 프로그래밍 방식으로 마이그레이션을 실행한다:

// src/db/migrate.ts
import { drizzle } from 'drizzle-orm/node-postgres';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
import { Pool } from 'pg';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const db = drizzle(pool);

async function main() {
  console.log('Running migrations...');
  await migrate(db, { migrationsFolder: './drizzle/migrations' });
  console.log('Migrations complete!');
  await pool.end();
}

main().catch(console.error);

Drizzle은 __drizzle_migrations 테이블을 자동 생성해 실행된 마이그레이션을 추적한다. 이미 실행된 마이그레이션은 건너뛴다.

push vs generate: 개발 vs 운영

명령 동작 사용 환경
drizzle-kit push 스키마를 DB에 직접 반영 (마이그레이션 파일 없음) 로컬 개발, 프로토타이핑
drizzle-kit generate SQL 마이그레이션 파일 생성 스테이징, 프로덕션
drizzle-kit studio 웹 UI로 DB 탐색/편집 디버깅, 데이터 확인
# 개발 환경: 빠른 반복
npx drizzle-kit push

# 운영 환경: 마이그레이션 파일로 관리
npx drizzle-kit generate
npx tsx src/db/migrate.ts

push는 Prisma의 db push와 유사하게 마이그레이션 이력 없이 스키마를 직접 동기화한다. 프로덕션에서는 절대 사용하지 말고, 반드시 generatemigrate 워크플로를 따라야 한다.

커스텀 마이그레이션: 데이터 변환

자동 생성된 마이그레이션만으로 부족한 경우 — 예를 들어 컬럼 분리, 데이터 변환이 필요할 때:

-- 0003_split_name_column.sql (수동 편집)
-- 자동 생성된 ALTER 문 대신 데이터 마이그레이션 포함

-- 1. 새 컬럼 추가
ALTER TABLE "users" ADD COLUMN "first_name" varchar(50);
ALTER TABLE "users" ADD COLUMN "last_name" varchar(50);

-- 2. 기존 데이터 변환
UPDATE "users"
SET first_name = split_part(name, ' ', 1),
    last_name = CASE
      WHEN position(' ' in name) > 0 THEN substring(name from position(' ' in name) + 1)
      ELSE ''
    END;

-- 3. NOT NULL 제약 추가
ALTER TABLE "users" ALTER COLUMN "first_name" SET NOT NULL;
ALTER TABLE "users" ALTER COLUMN "last_name" SET NOT NULL;

-- 4. 기존 컬럼 삭제
ALTER TABLE "users" DROP COLUMN "name";

자동 생성된 마이그레이션을 수정할 때는 스냅샷(meta/ 폴더)과 일관성을 유지해야 한다. Drizzle ORM 타입 안전 쿼리에서 다룬 것처럼, 스키마 변경 후에는 타입이 자동으로 갱신되므로 컴파일 타임에 불일치를 잡을 수 있다.

CI/CD 파이프라인 통합

# .github/workflows/migrate.yml
name: DB Migration
on:
  push:
    branches: [main]
    paths: ['drizzle/migrations/**']

jobs:
  migrate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Check pending migrations
        run: npx drizzle-kit check

      - name: Run migrations
        run: npx tsx src/db/migrate.ts
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}

drizzle-kit check는 스키마와 마이그레이션 파일의 일관성을 검증한다. CI에서 이를 먼저 실행해 마이그레이션 파일이 스키마 변경을 제대로 반영하는지 확인한다.

멀티 스키마와 테넌트 분리

// PostgreSQL 스키마별 테이블 정의
import { pgSchema } from 'drizzle-orm/pg-core';

export const tenantSchema = pgSchema('tenant_001');

export const tenantUsers = tenantSchema.table('users', {
  id: serial('id').primaryKey(),
  email: varchar('email', { length: 255 }).notNull(),
});

// drizzle.config.ts에서 스키마 지정
export default defineConfig({
  schema: './src/db/schema/*',
  schemaFilter: ['public', 'tenant_*'],  // 대상 스키마 필터
  // ...
});

멀티테넌트 환경에서 PostgreSQL 스키마를 활용하면 테넌트별 데이터를 격리하면서 마이그레이션을 통합 관리할 수 있다.

롤백 전략

Drizzle은 자동 롤백을 제공하지 않는다. 대신 순방향 마이그레이션(forward-only) 전략을 권장한다:

# 롤백이 필요한 경우:
# 1. 새 마이그레이션으로 되돌리기 (권장)
npx drizzle-kit generate   # 이전 스키마로 되돌리는 마이그레이션 생성

# 2. 수동 SQL 롤백 스크립트 유지 (대안)
# drizzle/rollbacks/0003_rollback.sql 를 수동 작성해 관리

프로덕션에서는 Read/Write 분리 환경에서 마이그레이션이 레플리카에 전파되는 시간을 고려해야 한다. 컬럼 추가 → 코드 배포 → 컬럼 삭제 순서의 확장-축소(expand-contract) 패턴을 적용하면 무중단 마이그레이션이 가능하다.

정리: Drizzle 마이그레이션 체크리스트

항목 권장
개발 환경 drizzle-kit push로 빠른 반복
운영 환경 generate → migrate 워크플로
CI 검증 drizzle-kit check 실행
파괴적 변경 strict: true + 수동 검토
데이터 변환 마이그레이션 SQL 직접 편집
롤백 순방향 마이그레이션 (expand-contract)
마이그레이션 파일 Git에 커밋, 코드 리뷰 필수

Drizzle의 마이그레이션은 순수 SQL + TypeScript 스키마의 조합이다. ORM이 마이그레이션을 추상화하지 않고 SQL을 그대로 노출하기 때문에, DBA 친화적이고 프로덕션 운영에서의 투명성이 높다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux