[NestJS] TypeORM 마이그레이션

cabbage·2023년 6월 16일
0

NestJS

목록 보기
13/17
post-thumbnail

NestJS로 배우는 백엔드 프로그래밍(한용재)에서 마이그레이션, TypeORM CLI를 활용해 마이그레이션하는 방법에 대해 공부했다. 마이그레이션이 무엇인지 알아보고, TypeORM CLI를 활용해 마이그레이션하는 방법을 정리한다. 마이그레이션을 진행하는 과정에서 많은 문제들이 있었는데, 다행히도 먼저 트러블슈팅을 기록해주신 분이 계셔서 참고하여 진행할 수 있었다. (참고했어도 오래 걸리긴 한듯..) 이번에 배운 내용들을 내가 아는 선에서 최대한 자세하게 적어보려고 노력했다.

마이그레이션이란

서비스를 개발하다 보면 데이터베이스 스키마를 변경할 일이 빈번하다. 새로운 기능을 추가하면서 테이블을 새롭게 추가하고 테이블 필드 이름이나 속성을 변경해야 할 일도 생긴다. 저장된 모든 데이터의 값을 수정할 일도 생긴다.

이런 것들을 마이그레이션이라고 한다. 마이그레이션은 이것보다 좀 더 넓은 의미의 용어지만 우선 TypeORM 마이그레이션 방법에 대해 공부하고 있기 때문에 이런 관점에서의 마이그레이션도 있다는 것만 알고 넘어간다.

TypeORM 마이그레이션 장점

TypeORM이 제공하는 마이그레이션의 장점들은 다음과 같다.

  • 마이그레이션을 위한 SQL문을 직접 작성하지 않아도 된다.
  • 마이그레이션이 잘못 적용되었을 때 마지막으로 적용한 마이그레이션을 되돌리는 작업도 간단한 명령어로 수행할 수 있다.
    • 데이터 값을 변경하는 마이그레이션이라면 원복 코드를 직접 작성해야 함
  • 마이그레이션 코드를 소스 코드로 관리할 수 있다.
    • 마이그레이션 코드의 코드 리뷰가 가능하다.
  • 마이그레이션 이력을 관리할 수 있다.
    • 언제 어떤 마이그레이션을 진행했는지를 특정 테이블에 기록하고, 필요한 경우 처음부터 순서대로 다시 마이그레이션을 수행할 수도 있다.

이처럼 TypeORM은 마이그레이션을 쉽고 안전하게 하는 방법을 제공한다.

TypeORM 마이그레이션 CLI 실행하기

아래 절차에 따라 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', // 마이그레이션 이력이 기록되는 테이블 이름 설정
}),
  • synchronize: false
    • 서버가 새로 구동될 때마다 테이블이 자동으로 생기지 않도록 하기 위해 false를 설정한다.
    • development 환경이라면 .development.env 파일의 DATABASE_SYNCHRONIZE 환경 변수의 값을 false로 설정한다.
  • migrationsRun: false
    • 서버가 구동될 때 마이그레이션 파일을 기반으로 마이그레이션을 수행하게 할지 설정하는 옵션이다.
    • 직접 CLI로 마이그레이션을 진행할 것이므로 false로 설정한다.
  • migrations
    • 마이그레이션을 수행할 파일이 관리되는 경로를 설정한다.
  • migrationsTableName
    • 마이그레이션 이력이 기록되는 테이블 이름을 설정한다.
    • default는 migrations이다.

DB 연결 도구(DBeaver)에서 User 테이블을 직접 삭제하고 서버를 재시동하면 synchronize 옵션이 false이므로 User 테이블이 생성되지 않는다.

마이그레이션 수행 파일을 생성하는 방법에는 두 가지가 있다.

  • migration:create
    • 수행할 마이그레이션 내용이 비어 있는 마이그레이션 파일을 생성한다.
    • 개발자가 직접 마이그레이션을 수행하고 되돌리는 SQL문을 작성해야 한다.
  • migration:generate
    • 소스 코드와 migrations 테이블에 기록된 이력을 바탕으로 마이그레이션 수행 파일을 자동 생성한다.
    • 현재 엔티티와 DB를 비교하여 둘을 동기화하기 위한 쿼리를 TypeORM이 자동으로 생성해준다.
    • Nest를 watch 모드로 실행 중이고 synchronize 옵션이 true인 경우 엔티티 소스 코드가 변경될 때마다 자동으로 동기화되기 때문에 마이그레이션 파일이 생성되지 않는 것에 주의한다.

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 메서드가 추가되었다.

  • up 메서드: 마이그레이션을 진행할 때 실행할 SQL문을 작성하는 메서드
  • down 메서드: 마이그레이션을 되돌릴 때 실행할 SQL문을 작성하는 메서드

마이그레이션을 실행하고 되돌리는 코드를 직접 작성하기 보다 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, down 메서드는 위에서 설명한 대로 동작한다.

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

  • User 테이블이 생성되었다.
  • migrations 테이블에 마이그레이션 이력이 기록되었다.

migration:revert 명령으로 마이그레이션을 되돌린다.

npm run typeorm:d migration:revert

  • User 테이블이 삭제되었다.
  • migrations 테이블에 저장된 마지막 마이그레이션 이력도 삭제되었다.

참고

profile
캐비지 개발 블로그입니다. :)

0개의 댓글