MikroORM Migration
데이터베이스 스키마는 코드와 함께 변한다. 새 Entity를 추가하거나, 기존 컬럼을 수정하거나, 테이블을 삭제할 때마다 DB 구조를 맞춰야 한다. 이 변경을 수동으로 SQL을 실행해서 관리하면, 환경마다 스키마가 달라지고 실수가 생긴다. Migration은 이 문제를 해결한다.
Migration이란
Migration은 데이터베이스 스키마 변경을 코드로 관리하는 방식이다. 각 변경 사항을 하나의 파일로 기록하고, 순서대로 실행하면 어떤 환경에서든 동일한 DB 구조를 재현할 수 있다.
Migration 1: 스키마 생성
Migration 2: store, device 테이블 생성
Migration 3: session, shot 테이블 추가
...
Migration 19: sales 테이블 추가
각 Migration에는 up(적용)과 down(되돌리기)이 있다. up은 변경을 적용하고, down은 변경을 취소한다.
Migration 파일 구조
MikroORM의 Migration 파일은 Migration 클래스를 상속하고, up과 down 메서드에 SQL을 작성한다.
스키마 생성
export class Migration20251127000001CreateSchema extends Migration {
up(): void {
this.addSql('CREATE SCHEMA IF NOT EXISTS "chiki";');
}
down(): void {
this.addSql('DROP SCHEMA IF EXISTS "chiki" CASCADE;');
}
}
PostgreSQL의 스키마를 생성한다. IF NOT EXISTS로 이미 존재하면 건너뛴다.
테이블 생성
export class Migration20251129042254 extends Migration {
override async up(): Promise<void> {
this.addSql(
`create table "chiki"."store" (
"store_id" uuid not null,
"name" varchar(255) not null,
"address" text not null,
"phone" varchar(50) null,
"status" text check ("status" in ('active', 'inactive', 'maintenance'))
not null default 'active',
"created_at" timestamptz not null default CURRENT_TIMESTAMP,
"updated_at" timestamptz not null default CURRENT_TIMESTAMP,
constraint "store_pkey" primary key ("store_id")
);`,
);
this.addSql(
`alter table "chiki"."device"
add constraint "device_serial_number_unique" unique ("serial_number");`,
);
}
override async down(): Promise<void> {
this.addSql(`drop table if exists "chiki"."store" cascade;`);
}
}
up에서 테이블을 생성하고, down에서 삭제한다. Entity의 @Property, @Enum, @PrimaryKey 등이 SQL의 컬럼 정의에 대응한다.
| Entity 데코레이터 | SQL |
|---|---|
@PrimaryKey({ type: 'uuid' }) | "store_id" uuid not null, constraint "store_pkey" primary key ("store_id") |
@Property({ length: 255 }) | "name" varchar(255) not null |
@Property({ nullable: true }) | "phone" varchar(50) null |
@Enum(() => StoreStatus) | "status" text check ("status" in (...)) |
@Property({ defaultRaw: 'CURRENT_TIMESTAMP' }) | default CURRENT_TIMESTAMP |
컬럼 추가/수정
기존 테이블에 컬럼을 추가하는 경우다.
export class Migration20260125102303 extends Migration {
override async up(): Promise<void> {
this.addSql(
`alter table "chiki"."frame_type" add column "data" jsonb null;`,
);
}
override async down(): Promise<void> {
this.addSql(`alter table "chiki"."frame_type" drop column "data";`);
}
}
ALTER TABLE로 기존 테이블 구조를 변경한다. down에서는 추가한 컬럼을 삭제해서 원래 상태로 되돌린다.
테이블 삭제
더 이상 필요 없는 테이블을 제거하는 경우다.
export class Migration20251229040334 extends Migration {
override async up(): Promise<void> {
this.addSql(`drop table if exists "chiki"."shot_video" cascade;`);
this.addSql(`drop table if exists "chiki"."shot_image" cascade;`);
}
override async down(): Promise<void> {
this.addSql(
`create table "chiki"."shot_video" (
"video_id" uuid not null,
"shot_id" uuid not null,
-- ... 이전 구조 복원
constraint "shot_video_pkey" primary key ("video_id")
);`,
);
}
}
up에서 테이블을 삭제하고, down에서 이전 구조를 다시 만든다. down에 이전 테이블 구조를 정확히 기록해야 되돌리기가 가능하다.
Migration 설정
database.config.ts에서 Migration 관련 설정을 정의한다.
const baseConfig = {
driver: PostgreSqlDriver,
entities: [Store, Device, Session, Shot, Payment, Sale, /* ... */],
extensions: [Migrator, SeedManager],
migrations: {
path: './src/database/migrations',
pathTs: './src/database/migrations',
snapshot: true,
},
schema: 'chiki',
};
| 설정 | 설명 |
|---|---|
extensions: [Migrator] | Migrator 확장 활성화 |
migrations.path | Migration 파일 저장 경로 |
migrations.pathTs | TypeScript Migration 파일 경로 |
migrations.snapshot | 스냅샷 파일 생성 여부 |
schema | PostgreSQL 스키마 이름 |
snapshot: true로 설정하면 .snapshot-chiki.json 파일이 생성된다. 이 파일은 현재 스키마 상태를 기록해서, 다음 Migration을 생성할 때 변경점을 자동으로 감지하는 데 사용된다.
Migration CLI 명령어
MikroORM CLI로 Migration을 생성하고 실행한다.
# Migration 생성 (Entity 변경 사항 자동 감지)
npx mikro-orm migration:create
# Migration 실행 (아직 적용되지 않은 것만)
npx mikro-orm migration:up
# 마지막 Migration 되돌리기
npx mikro-orm migration:down
migration:create는 현재 Entity 정의와 DB 스키마(또는 스냅샷)를 비교해서, 차이를 SQL로 만들어 새 Migration 파일을 생성한다. Entity에 @Property 하나를 추가하면 ALTER TABLE ... ADD COLUMN SQL이 자동 생성된다.
환경별 설정
로컬 개발 환경과 배포 환경에서 DB 설정이 다를 수 있다.
const environmentConfigs = {
local: {
...baseConfig,
clientUrl: process.env.DATABASE_URL,
schemaGenerator: {
disableForeignKeys: true,
createForeignKeyConstraints: false,
},
debug: true,
},
dev: {
...baseConfig,
clientUrl: process.env.DATABASE_URL,
driverOptions: {
connection: {
ssl: { ca: readFileSync('./ssl/ap-northeast-2-bundle.pem') },
},
},
debug: false,
},
};
| 환경 | 특징 |
|---|---|
| local | 외래 키 비활성화(테스트 편의), 디버그 로그 활성화 |
| dev | SSL 연결(AWS RDS), 디버그 비활성화 |
로컬에서 disableForeignKeys: true로 설정하면, Migration 실행 시 외래 키 순서를 신경 쓰지 않아도 된다. 프로덕션에서는 데이터 무결성을 위해 외래 키를 활성화한다.
Migration 실행 순서
MikroORM은 내부적으로 mikro_orm_migrations 테이블을 관리한다. 이 테이블에 이미 실행된 Migration의 이름과 시간이 기록된다.
| name | executed_at |
|-----------------------------------------|----------------------|
| Migration20251127000001CreateSchema | 2025-11-27 00:00:01 |
| Migration20251129042254 | 2025-11-29 04:22:54 |
| Migration20260206023507 | 2026-02-06 02:35:07 |
migration:up을 실행하면, 이 테이블에 없는 Migration만 시간순으로 실행한다. 이미 실행된 Migration은 건너뛴다. 새 팀원이 프로젝트를 클론해서 migration:up을 실행하면, 처음부터 모든 Migration이 순서대로 실행되어 최신 스키마가 만들어진다.
주의점
Migration 파일을 수정하지 않는다. 이미 다른 환경에서 실행된 Migration을 수정하면, 환경마다 스키마가 달라진다. 실수를 수정하려면 새 Migration을 만들어서 변경한다.
down을 정확하게 작성한다. up에서 테이블을 삭제했다면, down에서 해당 테이블의 이전 구조를 정확히 복원해야 한다. 그래야 문제 발생 시 안전하게 되돌릴 수 있다.
Entity 변경 후 Migration을 잊지 않는다. Entity를 수정하고 Migration을 생성하지 않으면, 코드와 DB 스키마가 맞지 않아 런타임 에러가 발생한다.
Pending Migration 트러블슈팅
위에서 설명한 대로, migration:up은 mikro_orm_migrations 테이블의 기록을 보고 "이미 실행한 것"과 "아직 안 한 것"을 구분한다. 이때 테이블에 기록이 없는 Migration 파일을 pending 상태라고 부른다. 정상적인 상황이라면 pending = 새로 추가된 Migration이므로, 실행하면 된다.
문제는 테이블은 이미 DB에 있는데, 기록에는 "안 했음"으로 남아있는 경우다. DB를 복원했거나, 누군가 SQL을 직접 실행했거나, 환경을 새로 세팅하면서 기록이 꼬일 수 있다. 이 상태에서 migration:up을 실행하면 이미 있는 테이블을 또 만들려고 시도한다.
error: relation "ai_models" already exists
MikroORM은 기록만 보기 때문에, 실제 DB 상태와 기록이 맞지 않으면 이런 충돌이 생긴다. 해결 방법은 간단하다. 이미 반영된 Migration의 기록만 수동으로 넣어주면 된다.
INSERT INTO <schema>.mikro_orm_migrations (name, executed_at)
VALUES
('Migration20260210001956', NOW()),
('Migration20260210022002', NOW());
이렇게 넣으면 MikroORM은 이 Migration들을 "이미 실행됨"으로 인식하고 건너뛴다. 이후 migration:up을 실행하면 진짜 새로운 Migration만 실행된다.
현재 pending 상태인 Migration은 다음 명령으로 확인할 수 있다.
npx mikro-orm migration:pending
정리
- Entity 변경 →
migration:create→migration:up순서를 습관화하면 환경 간 스키마 불일치를 원천 차단할 수 있다 - 이미 실행된 Migration 파일은 절대 수정하지 않고, 새 Migration으로 변경을 덮어쓰는 것이 원칙이다
- pending 상태 충돌은
mikro_orm_migrations테이블에 수동 INSERT로 해결하고, snapshot 파일을 활용하면 diff 자동 감지가 가능하다