NestJS로 배우는 백엔드 프로그래밍(한용재)에서 마이그레이션, TypeORM CLI를 활용해 마이그레이션하는 방법에 대해 공부했다. 마이그레이션이 무엇인지 알아보고, TypeORM CLI를 활용해 마이그레이션하는 방법을 정리한다. 마이그레이션을 진행하는 과정에서 많은 문제들이 있었는데, 다행히도 먼저 트러블슈팅을 기록해주신 분이 계셔서 참고하여 진행할 수 있었다. (참고했어도 오래 걸리긴 한듯..) 이번에 배운 내용들을 내가 아는 선에서 최대한 자세하게 적어보려고 노력했다.
서비스를 개발하다 보면 데이터베이스 스키마를 변경할 일이 빈번하다. 새로운 기능을 추가하면서 테이블을 새롭게 추가하고 테이블 필드 이름이나 속성을 변경해야 할 일도 생긴다. 저장된 모든 데이터의 값을 수정할 일도 생긴다.
이런 것들을 마이그레이션이라고 한다. 마이그레이션은 이것보다 좀 더 넓은 의미의 용어지만 우선 TypeORM 마이그레이션 방법에 대해 공부하고 있기 때문에 이런 관점에서의 마이그레이션도 있다는 것만 알고 넘어간다.
TypeORM이 제공하는 마이그레이션의 장점들은 다음과 같다.
이처럼 TypeORM은 마이그레이션을 쉽고 안전하게 하는 방법을 제공한다.
아래 절차에 따라 TypeORM 마이그레이션을 진행할 수 있었다. 책과는 다른 내용들이 있으니 참고해야 한다.
TypeORM 마이그레이션 CLI를 실행하기 위한 패키지를 먼저 설치한다.
npm i -g ts-node
ts-node를 사용해서 TypeORM CLI를 실행할 수 있는 환경을 구성한다. package.json의 scripts 항목에 코드를 추가한다.
// package.json
"scripts": {
...
"typeorm": "node -r ts-node/register ./node_modules/typeorm/cli.js",
"typeorm:d": "node -r ts-node/register ./node_modules/typeorm/cli.js -d ormconfig.ts"
}
여기서 scripts 항목에 추가한 코드는 책과 다르게 작성하였다. 이 명령어는 참고한 블로그에서 가져왔다.
typeorm
: DataSource 객체를 사용하지 않는 마이그레이션 명령어를 실행하기 위함typeorm:d
: DataSource 객체를 사용하는 마이그레이션 명령어를 실행하기 위함DataSource 객체는 DB 연결 정보를 담고 있는데, DB 연결 정보가 필요한 마이그레이션 명령어들이 존재한다. 이는 아래에서 다시 알아보자.
DB 연결 정보를 담고 있는 DataSource 객체를 제공하기 위해 ormconfig.ts
파일을 프로젝트 루트 디렉터리에 추가한다.
// ormconfig.ts
import { DataSource } from 'typeorm';
export const AppDataSource = new DataSource({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'test',
database: 'test',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: false,
migrations: [__dirname + '/**/migrations/*.js'],
migrationsTableName: 'migrations',
});
ormconfig.ts
에서 하드코딩된 값이 아니라 환경 변수를 사용할 경우 ConfigModule.forRoot
로 환경 변수를 읽어오기 전에 ormconfig.ts
파일이 컴파일되므로 에러가 발생한다. 이를 방지하려면 tsconfig.json
에서 컴파일 대상 소스를 다음과 같이 추가한다.
// tsconfig.json
{
...
"include": ["src/**/*"]
}
이제 TypeORM 마이그레이션을 CLI로 생성하고 실행할 수 있는 환경이 구성되었다.
다음은 마이그레이션 이력을 관리할 테이블을 설정해야 한다. AppModule에서 TypeOrmModule.forRoot 메서드에 전달하는 TypeOrmModuleOptions에 마이그레이션 관련 옵션을 추가한다.
TypeOrmModule.forRoot({
type: 'mysql',
host: process.env.DATABASE_HOST,
port: 3306,
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
database: 'test',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
logging: true,
synchronize: process.env.DATABASE_SYNCHRONIZE === 'true',
// 마이그레이션 이력을 관리할 테이블 설정(마이그레이션 관련 옵션들)
migrationsRun: false, // 서버 구동 시 작성된 마이그레이션 파일을 기반으로 마이그레이션을 수행하게 할지 설정하는 옵션. false로 설정하여 직접 CLI로 마이그레이션 수행
migrations: [__dirname + '/**/migrations/*.js}'], // 마이그레이션을 수행할 파일이 관리되는 경로 설정
migrationsTableName: 'migrations', // 마이그레이션 이력이 기록되는 테이블 이름 설정
}),
DB 연결 도구(DBeaver)에서 User 테이블을 직접 삭제하고 서버를 재시동하면 synchronize
옵션이 false이므로 User 테이블이 생성되지 않는다.
마이그레이션 수행 파일을 생성하는 방법에는 두 가지가 있다.
migration:create
migration:generate
migration:create
명령을 실행하면 수행할 마이그레이션 내용이 비어 있는 파일이 생성된다. 이때 package.json의 scripts 항목에 추가한 typeorm을 사용해야 한다. migration:create
명령은 소스 코드와 DB를 비교하는 과정 없이 단순하게 수행할 마이그레이션 내용이 비어 있는 파일을 생성해주기 때문에 DB 연결 정보를 담고 있는 DataSource 객체를 필요로 하지 않는다.
npm run typeorm migration:create src/migrations/CreateUserTable
import { MigrationInterface, QueryRunner } from "typeorm"
export class CreateUserTable1683037799847 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}
migration:create 명령 실행 시 설정한 이름과 파일 생성 시각(Unix)을 조합한 이름의 클래스가 생성되고, 그 내부에 up, down 메서드가 추가되었다.
마이그레이션을 실행하고 되돌리는 코드를 직접 작성하기 보다 TypeORM이 소스 코드와 migrations 테이블에 기록된 이력을 바탕으로 마이그레이션 파일을 자동으로 생성해주길 원하는 경우 migration:generate
명령을 실행한다. 이때 package.json의 scripts 항목에 추가한 typeorm:d을 사용해야 한다. 앞서 언급한 것처럼 현재 엔티티 소스 코드와 DB를 비교하여 둘을 동기화하기 위한 쿼리를 TypeORM이 자동으로 생성하기 때문에 DB 연결 정보가 필요하다. 따라서 DataSource 객체를 제공해야 한다.
npm run typeorm:d migration:generate src/migrations/CreateUserTable
import { MigrationInterface, QueryRunner } from "typeorm";
export class CreateUserTable1683038044789 implements MigrationInterface {
name = 'CreateUserTable1683038044789'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE \`User\` (\`id\` varchar(255) NOT NULL, \`name\` varchar(30) NOT NULL, \`email\` varchar(60) NOT NULL, \`password\` varchar(30) NOT NULL, \`signupVerifyToken\` varchar(60) NOT NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE \`User\``);
}
}
소스 코드 내에서 user.entity.ts 파일과 migrationsTableName 옵션으로 설정된 migrations 테이블을 조회한 결과를 비교한 마이그레이션 파일이 생성되었다.
up 메서드 내부에는 User 테이블을 생성하는 SQL문을 볼 수 있고, down 메서드 내부에는 User 테이블을 제거하는 SQL문을 볼 수 있다. 이것은 현재 DB에 User 테이블이 삭제되어서 없지만 소스 코드에는 user.entity.ts 파일이 존재하기 때문이다. 결국 마이그레이션을 실행하면 up 메서드가 실행되어서 User 테이블이 DB에 생성될 것이고, 마이그레이션을 되돌리면 down 메서드가 실행되어서 User 테이블이 DB에서 제거될 것이다.
migration:run
명령으로 마이그레이션을 수행한다.
npm run typeorm:d migration:run
migration:revert
명령으로 마이그레이션을 되돌린다.
npm run typeorm:d migration:revert