들어가며
사이드 프로젝트에서 푸시 알림을 활용한 서비스를 개발하고 있습니다. 그 과정에서 생각하고 배웠던 점들을 하나씩 작성하고자 합니다. 먼저 푸시 알림에 필요한 로직을 테스트하기 위해서는 먼저 데이터베이스 세팅을 해야 합니다. 이 글은 NestJS로 배우는 백엔드 프로그래밍 글을 참고한 내용입니다. 링크에서 더 자세하게 내용을 살펴볼 수 있습니다.
마이그레이션
넓은 의미의 마이그레이션은 애플리케이션이 구동되는 OS를 바꾸거나 데이터베이스를 MySQL에서 Oracle로 바꾸는 것과 같이 인프라를 교체하는 것을 포함합니다. 데이터베이스를 다룰 때에도 마이그레이션이라는 용어를 자주 접하게 됩니다. 서비스를 개발하다 보면 데이터베이스 스키마를 변경할 일이 빈번하게 발생합니다. 신기능을 추가하면서 새로운 테이블을 생성하기도 하고 테이블 필드의 이름이나 속성을 변경해야 하는 일도 생깁니다. 만약 이전에 저장해 둔 데이터가 현재의 도메인 구조와 다르다면 모든 데이터의 값을 수정할 일도 생깁니다. 이런 과정 역시 마이그레이션이라고 부릅니다.
TypeORM은 마이그레이션을 쉽고 안전하게 할 수 있는 방법을 제공합니다. 첫째, 마이그레이션을 위한 SQL문을 직접 작성하지 않아도 됩니다. 만약 마이그레이션이 잘못 적용됐다면 마지막 적용한 마이그레이션 사항을 되돌리는 작업도 간단히 명령어로 수행할 수 있습니다. 물론 데이터의 값을 변경하는 마이그레이션이라면 원복 하는 코드를 직접 작성해야 하기는 합니다. 그리고 롤링 업데이트가 가능한 마이그레이션이 아니라면 적용하기 전에 DB를 백업하는 것을 잊지 말아야 합니다.
둘째, TypeORM 마이그레이션을 이용하면 마이그레이션 코드를 일정한 형식으로 소스 저장소에서 관리할 수 있습니다. 즉 데이터베이스의 변경점을 소스 코드로 관리할 수 있다는 뜻입니다. 소스코드로 관리할 때의 또 다른 장점은 코드 리뷰가 가능하다는 점입니다. 마이그레이션 코드가 자동 생성되기는 하지만 직접 추가 코드를 넣을 경우도 생깁니다. 사내 코드 리뷰 프로세스에 따라 리뷰어가 승인한 코드는 더 탄탄한 코드가 됩니다. 물론 SQL문을 직접 작성해서 특정 디렉터리에 모아서 보관할 수도 있겠지만 TypeORM이 제공하는 일관된 형식을 사용하면 이미 해당 지식을 가진 신규 개발자가 쉽게 적응할 수 있습니다.
마지막으로 TypeORM 마이그레이션을 적용하여 마이그레이션 이력을 관리할 수 있습니다. 언제 어떤 마이그레이션이 일어났는지를 특정 테이블에 기록하고, 필요한 경우 처음부터 순서대로 다시 수행할 수도 있습니다.
그럼 이제 TypeORM 마이그레이션을 적용해 보겠습니다. 먼저 migration CLI로 명령어를 수행해야 하므로 필요한 패키지를 설치해 줍니다. TypeORM CLI는 TypeORM 패키지에 포함되어 있고, TypeORM CLI는 타입 스크립트로 작성된 엔티티 파일을 읽어 들입니다. 따라서 TypeORM CLI를 실행하기 위해 ts-node 패키지를 글로벌 환경으로 설치합니다.
$ npm i -g ts-node
이제 ts-node를 이용해서 우리 프로젝트 디렉터리 내에서 npm run typeorm 명령으로 typeorm CLI를 실행할 수 있는 환경을 구성합니다. package.json 파일에 다음 코드를 추가하겠습니다.
"scripts": {
...
"typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js"
}
이러면 마이그레이션을 CLI로 생성하고, 실행할 수 있는 환경이 구성됐습니다. 다음 할 일은 마이그레이션 이력을 관리할 테이블을 설정해야 합니다. ormconfig.json에서 마이그레이션 관련 옵션을 추가합니다.
{
...
"synchronize": false,
"migrations": ["dist/migrations/*{.ts,.js}"],
"cli": {
"migrationsDir": "src/migrations"
},
"migrationsTableName": "migrations"
}
마이그레이션 테스트를 하기 위해 synchronize를 false로 변경합니다. 그렇지 않으면 서버가 새로 구동될 때마다 테이블이 자동으로 생겨서 불편합니다. 또한 migrations를 통해 마이그레이션을 수행할 파일의 경로를 설정할 수 있고, cli 옵션을 통해 마이그레이션 파일을 생성할 파일의 디렉터리를 설정합니다. 그리고 migrationsTableName을 통해 마이그레이션 이력이 기록되는 테이블 이름을 설정할 수 있습니다. 만약 생략한다면 기본값은 migrations가 됩니다.
그리고 마이그레이션 파일을 생성하는 방법은 2가지가 있습니다.
- migration:create : 수행할 마이그레이션 내용이 비어있는 파일을 생성합니다.
- migration:generate : 현재 소스코드와 migrations 테이블에 기록된 이력을 기반으로 마이그레이션 파일을 자동 생성합니다.
먼저 migration:create 명령어를 사용해 보겠습니다. 먼저 -n 옵션은 생성될 파일의 이름과 마이그레이션 이력에 사용됩니다.
$ npm run typeorm migration:create -- -n CreateUserTable
위와 같이 입력하면 아래의 파일이 생성됩니다.
import { MigrationInterface, QueryRunner } from "typeorm";
export class CreateUserTable1640441100470 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}
class의 이름은 migration:create 명령의 -n 옵션으로 설정한 이름과 파일 생성 시각을 조합한 이름을 가진 클래스가 생성됩니다. up 함수는 migration:run 명령으로 마이그레이션이 수행될 때 실행되는 코드를 작성합니다. 그리고 down 함수는 miration:revert 명령으로 마이그레이션을 되돌릴 때 실행되는 코드를 작성합니다.
우리는 마이그레이션을 실행하고, 되돌리는 코드를 직접 작성하는 것을 원치 않습니다. 생성된 파일을 삭제하고 migration:generate 명령으로 새로운 파일을 생성해 보겠습니다.
$ npm run typeorm migration:generate -- -n CreateUserTable
import { MigrationInterface, QueryRunner } from "typeorm";
export class CreateUserTable1640441100470 implements MigrationInterface {
name = 'CreateUserTable1640441100470'
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\``);
}
}
소스 코드 내에서 entity.ts 파일과 migrationsTableName 옵션으로 설정된 migrations 테이블을 조회한 결과를 비교한 마이그레이션 파일이 생성되었습니다. 이제 마이그레이션을 수행해 봅시다. 먼저 migration:run으로 테이블을 생성합니다.
$ npm run typeorm migration:run
> book-nestjs-backend@1.0.0 typeorm /Users/dextto/src/nestjs/book-nestjs-backend/user-service/ch8-database
> node --require ts-node/register ./node_modules/typeorm/cli.js "migration:run"
query: SELECT * FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = 'test' AND `TABLE_NAME` = 'migrations'
query: SELECT * FROM `test`.`migrations` `migrations` ORDER BY `id` DESC
0 migrations are already loaded in the database.
1 migrations were found in the source code.
1 migrations are new migrations that needs to be executed.
query: START TRANSACTION
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
query: INSERT INTO `test`.`migrations`(`timestamp`, `name`) VALUES (?, ?) -- PARAMETERS: [1640441100470,"CreateUserTable1640441100470"]
Migration CreateUserTable1640441100470 has been executed successfully.
query: COMMIT
migratons 테이블에 마이그레이션 이력이 기록되어 있음을 확인할 수 있습니다.
이제 다시 마지막 적용된 마이그레이션을 되돌려 봅시다. 적용된 게 하나밖에 없으므로 유저 테이블을 삭제하는 코드가 수행됩니다.
npm run typeorm migration:revert
> book-nestjs-backend@1.0.0 typeorm /Users/dextto/src/nestjs/book-nestjs-backend/user-service/ch8-database
> node --require ts-node/register ./node_modules/typeorm/cli.js "migration:revert"
query: SELECT * FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = 'test' AND `TABLE_NAME` = 'migrations'
query: SELECT * FROM `test`.`migrations` `migrations` ORDER BY `id` DESC
1 migrations are already loaded in the database.
CreateUserTable1640441100470 is the last executed migration. It was executed on Sat Dec 25 2021 23:05:00 GMT+0900 (GMT+09:00).
Now reverting it...
query: START TRANSACTION
query: DROP TABLE `User`
query: DELETE FROM `test`.`migrations` WHERE `timestamp` = ? AND `name` = ? -- PARAMETERS: [1640441100470,"CreateUserTable1640441100470"]
Migration CreateUserTable1640441100470 has been reverted successfully.
query: COMMIT
다시 migratons 테이블을 확인하면 마지막 이력이 삭제되어 있습니다.
No changes in database schema were found - cannot generate a migration. To create a new empty migration use “typeorm migration:create” command
만약 작업을 하다가, 경로 설정이 제대로 안된 경우에는 다음과 같은 로그가 찍힐 수 있습니다. 이런 로그가 찍힌다면, 경로 설정에 대해 다시 살펴봐야 합니다.
또한 마이그레이션을 할 때 generate를 활용할 경우, entity 모델과 DB를 비교해서 업데이트 부분을 알아서 작성해주는데, 조심해야할 것은 컬럼의 길이, 타입 변경이 된다면 해당 테이블의 컬럼을 DROP 한 뒤 재 생성하는 방식으로 이뤄지기 때문에 기존 컬럼에 있던 데이터 전부가 날아갈 수 있는 문제가 생길 수 있습니다. 이 부분을 조심해서 활용해야 합니다. 만약 generate를 활용해서 마이그레이션 파일을 만들더라도, 테이블을 DROP 하는지의 여부를 잘 파악해서 사용하는 것이 중요합니다.
마이그레이션을 설정하면서, TypeORM의 synchronize 설정으로 무지성으로 테이블 설정을 해왔다는 것을 반성할 수 있었습니다. 만약 synchronize 설정을 활용하게 된다면, 잘못하면 테이블의 데이터를 모두 날려버릴 수도 있는 큰 문제가 생길 수 있기 때문에, 앞으로도 생각 없이 적용한 것들이 어떤 파급효과를 일으킬 수 있을지에 대해 계속해서 고민해야겠다고 생각했습니다.
지금까지 마이그레이션을 설정했다면, 다음에는 seeding, faker 라이브러리를 활용해서 데이터베이스에 더미 데이터를 넣고, 쿼리 성능을 높이기 위해 노력하는 방법에 대해 알아보겠습니다.
마치며
NestJS를 공부하면서 배운 내용들을 앞으로도 꾸준히 작성하고 싶습니다. 좋은 개발자가 되기 위해 꾸준히 공부하겠습니다.
출처
'Project > 서버 개발' 카테고리의 다른 글
[Project] 프로젝트 삽질기6 (feat PostgreSQL 버전) (0) | 2022.03.11 |
---|---|
[Project] 프로젝트 삽질기5 (feat Seeding, Faker) (0) | 2022.03.11 |
[Project] 프로젝트 삽질기3 (feat Docker, PostgreSQL) (0) | 2022.03.10 |
[Project] 프로젝트 삽질기2 (feat MySQL, PostgreSQL) (0) | 2022.03.10 |
[Project] 프로젝트 삽질기1 (feat FCM 공식문서) (2) | 2022.03.06 |