Thumbnail

3분

날렸습니다...

데이터를 날렸습니다.

비록 몇 개의 글이 없었지만, 이 경험을 통해 또 하나를 배워갑니다.

발생

신나게 새로운 포스트를 작성할 때, 임시저장 시 500 에러가 발생했습니다. 로그를 확인해보니 utf8mb4 타입을 지정해주지 않아 생긴 문제였습니다. 이모지 사용 때문에 인식하지 못한 문제였습니다.

처음에는 컬럼 길이보다 텍스트 길이가 길어서 문제가 생겼다고 생각했고, 컬럼 타입을 text에서 longtext로 변경했습니다. 개발 시에는 synchronize를 통해 자동으로 DB 업데이트가 되었기 때문에 문제가 없었습니다.

하지만 문제의 원인은 다른 곳에 있었습니다. typeorm connect 시 config 설정에서 charset을 utf8mb4로 지정해주니 오류 없이 실행되었습니다. 정상 작동을 위해 백엔드 배포를 하고 pm2를 재시작했습니다. 그러나 기존 글을 확인하려고 할 때, 빈 내용만 나오는 화면을 보고 문제가 있다고 직감했습니다.

아(Ah)... synchronize

원인

ORM으로 TypeORM을 사용하고 있으며, DB는 MariaDB를 사용했습니다.

ORM은 자바스크립트로 작성된 Entity를 DB 연동해줘서 제공된 함수나 클래스를 통해 쉽게 DB 쿼리 작업을 진행할 수 있습니다.

그 중 Synchronize(sync)는 자바스크립트로 작성된 Entity를 통해 schema를 생성, 수정, 삭제 등을 해주는 작업을 말합니다.

문제는 Sync 설정을 끄지 않고 Production으로 진행하고 있었다는 것이었습니다.

개발 시에는 편의성을 위해 Sync은 아주 좋은 옵션입니다. 실제 Schema를 신경쓰지 않고 Entity만 신경쓰면 됩니다.

결국은 새롭게 컬럼 타입이 변경되어 alter column이 되는 순간 기존 column은 사라지고 새로운 컬럼 타입을 가진 column이 생성되어 해당 컬럼에 있던 데이터가 싹 날아가는 일이 벌어졌습니다.

Markdown을 저장하는 컬럼이 변경되었기 때문에 본문을 저장한 데이터는 날아갔지만 글은 남아있었고 리스트는 보이지만 실제 내용은 보이지 않는 상태로 되었던 것이었습니다.

해결방식(Sad Ending)

구글링을 통해 MySQL, MariaDB 복구 글을 몇개 찾다 보니 flashback이라는게 보였습니다.

MariaDB 10.2.x 버전 부터 제공되는 기능으로 binlog를 통해서 스키마, 데이터 등을 복구할 수 있는 기능입니다.

아! binlog를 찾아봐야지! 아! 없네...

MariaDB의 flashback 기능을 이용하여 복구를 시도했지만, DB 설정이 올바르게 되어 있어야 binlog가 저장된다는 것을 알았습니다. 설정이 되어 있지 않아서 binlog가 생성되지 않았고, 결국 복구를 포기했습니다.

[mysqld]
# Log Config
binlog_format                   = 2
expire_logs_days                = 10
long_query_time                 = 10
max_binlog_size                 = 512M
sync_binlog                     = 1
log-bin                         = mysql-bin
log-error                       = mysql.err
datadir                         = /var/lib/mysql/log

위와 같이 binlog를 위한 log config 설정을 해주고 db를 실행해야 binlog가 생성됩니다.

저는 아~주 plain config만 설정하였기 때문에 있을리가 없었죠.

우선 bin log가 존재를 해야 가능하다. bin log조차 없다면 깔끔하게 포기~~!!
- 어느 블로그

binlog를 통해 백업하는 방법은 mysqlbinlog를 통해 sql을 생성해서 동작시키면 되는 방법입니다.

$ mysqlbinlog /var/log/mysql/mybin-log.000001 > restore.sql

그래서 뭐...깔끔하게 포기하고 일단 나중에 상황을 대비해서 binlog 설정과 다른 config 설정들을 마치고 docker 설정도 몇가지 더 하고 배포했습니다.

TypeORM Migration

synchronize 기능은 개발 시엔 편하지만 운영 시엔 매우 주의깊에 쓰거나 사용하지 말아야 합니다. 왜냐하면 저 처럼 될 수 있으니깐요 ^^..

스키마를 수정할 시에는 데이터 또한 수정되거나 삭제 되어버릴 수 있기 때문에 매우 조심스럽게 해야합니다.

데이터를 보호하기 위해 TypeORM의 migration 기능을 사용할 수 있습니다. 이를 통해 스키마를 안전하게 수정할 수 있습니다.

1. ORMConfig 설정

module.exports = {
  cli: {
    migrationsDir: join(__dirname, "/migrations"),
  },
}

cli migrationDir 경로를 지정해주고 마이그레이션 코드를 생성해줍니다.

2. migration:create

typeorm migration:create -n [whateverYouWantName]

script로 만들어 쓰도록 합시다

"typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js --config ./src/ormconfig.ts",
"migration": "yarn typeorm migration:run",
"migration:create": "yarn typeorm migration:create -n",
"migration:revert": "yarn typeorm migration:revert"

migration:generate 기능도 있지만 table을 drop 후 create하는 방식을 사용하기 때문에 create방식으로 쓰도록 하자.

-Migration generation drops and creates columns instead of altering resulting in data loss

그 후 migration 코드를 작성하면 끝.

3. Migration 코드 구현

생성된 migration 코드에 DDL을 작성합니다.

import { MigrationInterface, QueryRunner } from 'typeorm';
 
export class NewMigrationTIMESTAMP implements MigrationInterface {
  async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`ALTER TABLE "post" ALTER COLUMN "title" RENAME TO "name"`);
  }
 
  async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`ALTER TABLE "post" ALTER COLUMN "name" RENAME TO "title"`);
  }
}

4. 실행 및 되돌리기

# up 메서드 실행
typeorm migration:run
 
# down 메서드 실행
typeorm migration:revert

마치며

개인적으로 운영하는 DB라서 다행이지만, 실제 서비스되는 DB에서 이런 문제가 발생했다면 큰 문제였을 것입니다.

  1. 개발 환경에서는 편리한 synchronize 기능을 사용하지만, 운영 환경에서는 절대 사용하지 않도록 주의해야 합니다.
  2. 데이터 변경이 있는 스키마 업데이트 작업은 TypeORM의 migration 기능을 활용하여 안전하게 진행해야 합니다.
  3. DB 설정을 올바르게 하여, 필요한 로그 및 백업 기능을 활성화해야 합니다. 이를 통해 데이터 손실이 발생했을 때 복구할 수 있는 방법을 마련해야 합니다.

이러한 경험을 통해 앞으로 더 신중하게 작업할 수 있게 되었습니다.

reference

마지막 업데이트

7/16/2021


Avatar

JHSeo

배우는 것을 좋아하고 관심이 많은 웹 엔지니어 입니다. 느리더라도 꾸준하게 성장하려고 노력하는 개발자입니다.