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와 유사하게 마이그레이션 이력 없이 스키마를 직접 동기화한다. 프로덕션에서는 절대 사용하지 말고, 반드시 generate → migrate 워크플로를 따라야 한다.
커스텀 마이그레이션: 데이터 변환
자동 생성된 마이그레이션만으로 부족한 경우 — 예를 들어 컬럼 분리, 데이터 변환이 필요할 때:
-- 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 친화적이고 프로덕션 운영에서의 투명성이 높다.