사용동기
서비스를 하다보면 기획이 추가돼서 Production 환경의 Database 테이블을 추가/변경 해야하는 상황이 정말 많다. 이 때 서버 개발자인 나는 운영 환경과, 개발 환경의 DB 모델을 동기화해야 하는데 어떤 것이 Best practice 인지 잘 몰라서 직접 Postico로 쿼리를 날려 변경했었다. 근데 컬럼이 추가되거나 수정될 때마다 일일히 쿼리를 날리다보니 수정 사항이 많아질 때, 개발 환경과 운영 환경의 모델이 동기화되어 있는지 확신이 들지 않았다. 그러던 도중 TypeORM CLI로 운영 환경의 DB 모델을 코드로써 변경할 수 있다는 것을 알게 되었고, 현 시스템에 적용하게 되었다.
뼈 아픈 기억들
이전에도 TypeORM의 Synchronize: true 옵션으로 데이터를 날려먹은 경험이 있어서 기획이 추가될 때마다 두려웠다. 실제로 그렇게 해서 20개 가량되는 리뷰 데이터를 날렸었다(다행히 백업 DB가 있어서 복구할 수 있었다). 이런 경험이 쌓이면서 코드로써 데이터베이스 변경 사항을 스냅샷으로 저장하고, 변경을 취소하고 싶을 때는 Revert 할수 있는 시스템으로 바꾸고 싶었다. 마침내 TypeORM migration을 찾아 적용했는데 매우 만족스럽다.
TypeORM의 Migration 이란?
시작하기
1. Migration 파일을 생성하기 전에 typeorm의 connection option을 먼저 설정해주자.
// ormconfig.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
import './src/env';
const ormconfig: TypeOrmModuleOptions = {
type: 'postgres',
username: process.env.RDS_USERNAME,
password: process.env.RDS_PASSWORD,
host: process.env.RDS_HOSTNAME,
port: +process.env.RDS_PORT,
database: process.env.RDS_DB_NAME,
ssl: false,
synchronize: false,
logging: process.env.NODE_ENV === 'dev',
keepConnectionAlive: true,
namingStrategy: new SnakeNamingStrategy(),
entities: [__dirname + '/**/*.entity.{js,ts}'],
subscribers: ['src/subscriber/**/*.ts'],
migrations: [__dirname + '/database/migrations/**/*.ts'],
migrationsTableName: 'migrations',
cli: {
entitiesDir: __dirname + '/**/*.entity.{js,ts}',
migrationsDir: __dirname + '/database/migrations/',
subscribersDir: 'src/subscriber',
},
};
export default ormconfig;
cli.migrationDir 속성은 생성될 migration 파일들이 생겨날 디렉토리이다.
export default로 해야 'typeorm migrate:create' 을 실행할 때 cli.migrationDir 속성이 적용된다.
2. package.json script에 밑의 3가지 TypeORM CLI을 추가.
{
"migrate:create": "ts-node ./node_modules/typeorm/cli.js migration:create -n Test",
"migrate:run": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:run",
"migrate:revert": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:revert",
}
typeorm migrate:create
- 실행 시 cli.migrationDir 디렉토리 밑에 Migration 파일이 생성된다.
- 뒤의 'Test' 인자는 파일 이름 생성시에 사용됨.
typeorm migrate:run
- migration 파일에 적힌 쿼리를 실행한다.
- DB에 migrations이라는 table을 생성하고, 시간 순서대로 마이그레이션 정보를 저장한다.
typeorm migrate:revert
- typeorm migrate:run 실행으로 변경되기 전으로 돌아간다.
- migrations 테이블에 저장된 마이그레이션 데이터를 최신 순으로 하나씩 제거한다.
- 두번째 전으로 돌아가려면 revert를 2번 해야한다.
각 CLI에 ts-node 를 추가한 이유는 ormconfig.ts 파일을 읽기 위하여 js 파일로 먼저 트랜스파일 해야하기 때문이다. 추가하지 않으면 ormconfig.ts를 읽지 못하기 때문에 해당 파일에서 정의한 cli.migration path에 생기지 않고 root path에 생겨버린다.
또한 Migrate:revert CLI가 'ts-node -r tsconfig-paths/register'인 이유는 typeorm이 tsconfig.json 파일에서 정의한 타입 엘리어스 경로를 읽지 못하기 때문이다(Jest cli와 이유가 같음). 그래서 tsconfig-paths npm 모듈을 사용해 절대 경로로 변경한 후에 typeorm cli를 사용해야한다.
tsconfig-paths 실행 없이 'ts-node ./node_modules/typeorm/cli.js migration:revert' 이렇게 하면 아래와 같이 typeorm이 정의한 @room/entities 경로를 읽지 못한다.
3. Migration 파일 생성
CLI에 대한 설명이 끝났으니 Migration 파일을 직접 생성해보자.
npm run migrate:create
아래와 같이 파일 이름이 timestamp로 생성된다. 그리고 up, down 메소드를 갖는 클래스가 생성된다.
- up 메소드는 typeorm migrate:run 실행 시에 실행된다.
- down 메소드는 typeorm migrate:revert 실행 시에 실행된다.
import { MigrationInterface, QueryRunner } from 'typeorm';
export class Test1640766854205 implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
"CREATE TYPE place_type_enum AS ENUM ('All', 'Regular-meeting', 'Lightning', 'Event')",
);
await queryRunner.query(
"ALTER TABLE places ADD COLUMN place_type place_type_enum DEFAULT 'All'",
);
}
async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP TYPE place_type_enum');
await queryRunner.query('ALTER TABLE places DROP COLUMN place_type');
}
}
나는 places 테이블에 place_type_enum 이라는 enum 타입을 생성하고, 그 타입으로 place_type Column을 생성하고 싶었기에 위와 같이 작성했다.
4. Migration run으로 적용해보자.
npm run migrate:run
5. 결과 터미널
migration 파일의 up 메소드가 실제 DB에서 똑같이 실행되었다👏🏻👏🏻
이렇게 typeorm cli를 사용하면 테이블에 변화 과정을 코드로 시간 순서대로 볼 수도있고, 언제든지 Revert할 수 있다.
6. Migration 최종 확인
테이블도 확인해보면 시간 순서대로 migration을 table에 데이터로서 저장하여 언제든지 revert할 수 있게 typeorm migration cli가 도와준다.
잘 적용했다면 앞으로 DDL 변경으로 인해 데이터 잃어버릴 일은 없을 것이다!
참조
https://orkhan.gitbook.io/typeorm/docs/migrations#creating-a-new-migration
https://github.com/typeorm/typeorm/issues/1556
https://www.npmjs.com/package/tsconfig-paths
https://www.postgresql.org/docs/9.1/datatype-enum.html