DB 테이블 백업(2) - cron, fs모듈

Robin·2024년 8월 14일

이전글 바로가기 👉 [ DB 테이블 백업(1) - mysqldump ]

호봄 테크 블로그의 게시글 테이블 백업 작업중이다.

이전 글에서처럼 mysqldump를 통해 백업할 방도는 구했으나, 이 백업을 어떻게 관리할지는 정해지지 않았다. 이에 아래는 프론트메이트와 함께 정한 관리 방식이다:

  1. 매주 일요일 자정 게시물 테이블 백업
  2. 14일 이상 지난 백업 파일은 삭제

-들어가기전 이전글에서의 수정사항-
이전 글에서는 무작정 process.cwd() + ../에 백업 SQL 파일이 쌓이게 하였다. 아무래도 백업 자체를 구현하는데에 집중하다보니 그렇게 되어버렸다.

현재는 백업 디렉토리를 따로 만들어 그곳에 파일들이 쌓이게끔 하였다. 이에 fs모듈의 mkdir, access 백업 디렉토리의 존재를 확인하는 로직을 추가하여 백업이 실행되게끔 하였다.

매주 백업 - cron

노드에도 cron job을 적용하면 좋지 않을까 싶어 node-cron을 활용했다. 사실 스프링 프로젝트를 진행하며 Quartz+Batch 조합을 써봤기에 초기 러닝커브가 높을까봐 지레 겁먹었는데, 공식문서가 잘 쓰여져있어 정말 사용하기 쉬웠다.

이전글에서 작성한 스크립트를 cron schedule에 넣어 작동시켰다.

import path from "path";
import { execFile } from "child_process";
import cron from "node-cron";

const backupScriptPath = path.join(__dirname, "backup.script.js");

// 매주 토요일에서 일요일 넘어가는 자정 실행
const runBackupScript = cron.schedule(
  "0 0 * * 0",
  () => {
    execFile("node", [backupScriptPath], (error, stdout, stderr) => {
      if (error) {
        console.error(`Error executing backup script: ${error}`);
        return;
      }
      if (stderr) {
        console.error(`Backup script stderr: ${stderr}`);
      }
      console.log(`Backup script output: ${stdout}`);
    });
  },
  {
    scheduled: false,
    timezone: "Asia/Seoul",
  },
);

export default runBackupScript;

error는 명령 자체가 실행되지 않았을 경우인지라 return하였고, stderr는 실행되는 동안 발생한 오류인지라 이미 실행될 가능성을 고려하여 stdout까지 확인하게 하였다.

schedule: false인 이유는 app.ts에서 listening 될 때 runBackupScript.start();이와 같이 함께 실행시키기 위함이다.

timezone의 경우, 공식문서에서 제공하는 타임존에 서울이 있기에 명시해줬다!

cleanup_일정 기간이 지난 백업파일 삭제 - fs모듈

매주 백업을 하게끔 설정하고 나니, 한정된 메모리에 무작정 백업파일을 쌓을 순 없었다.

깔끔하게 특정기간의 SQL 파일만 쌓아있길 원했기에 로그 파일처럼 압축 파일을 만들고 싶진 않았다.
그렇게 나온 결론이 매주 14일이 지난 파일을 체크하여 삭제하는 것이었다.

이전 직장에서 파일 배포 시스템을 구현한 경험이 있는데, 그때 노드 fs모듈 stat을 통해 파일 생성시간을 확인할 수 있는 점을 인지하고 있었다. 이점을 이용해서 코드를 작성해보았다.

import { readdir, stat, unlink } from "fs/promises";
import { BACKUP_DIR } from "../common/constant";
import path from "path";

const MAX_AGE_DAYS = 14;

async function removeOldBackups() {
  try {
    const files = await readdir(BACKUP_DIR);

    const removePromises = files.map(async (file) => {
      const filePath = path.join(BACKUP_DIR, file);
      const fileStat = await stat(filePath);

      const now = Date.now();
      const fileBirthTime = fileStat.birthtimeMs || fileStat.ctimeMs;

      const fileAgeInDay = (now - fileBirthTime) / (1000 * 60 * 60 * 24);

      if (fileAgeInDay > MAX_AGE_DAYS) {
        await unlink(filePath);
        console.log("Deleted old backup file:", file);
      }
    });

    await Promise.all(removePromises);
    console.log("Deleted old backup files completed.");
  } catch (error) {
    console.log("Error removing old backups:", error);
  }
}

export default removeOldBackups;

밀리초로 계산하기 위해 stat에서 제공하는 birthtimeMsctimeMs를 활용했다.

  • birthtimeMs: 파일이 처음 생성된 시간
  • ctimeMs: 파일의 상태가 변경된 시간 (내용이 변경되었을때는 mtimeMs를 사용한다)

가끔 birthtimeMs는 특정 파일에서 제공하지 않는 이슈가 있다고 한다. 이에 ctimeMs를 추가하여 생성시간의 근사치를 구했다.

같은 시간이지만 별도의 cron job으로 둔 이유

이렇게 작성한 cleanup 코드도 마찬가지로 위와 비슷한 크론잡을 돌려 매주 진행되게끔 설정했다.

같은 시간에 진행되지만 별도의 잡으로 둔 이유가 있다. 우선 어찌됐든 관심사 분리가 컸다. 또한 백업과 클린업을 같은 작업으로 돌리게 될 시 서로가 서로에게 끼치는 영향을 고려했다.

만약 클린업 해야할 파일의 양이 많다면? 애꿎은 백업은 늦춰지게 된다.
만약 백업 중에 에러가 난다면? 애꿎은 클린업도 수행되지 않을 수 있다.

이러한 점을 고려하니 별도로 관리하고 유연성을 가져가는 것이 낫겠다는 생각이 들었다.


이제 할 건 이전 호봄 프로젝트에서처럼 에러 시 알람 보내기. 기존처럼 디스코드로 보낼까, 아니면 이메일로 보내볼까, 고민중이다. 자바 스프링 프로젝트도 리팩토링하고 싶어서 요리조리 왔다갔다하면서 코드 치니 재밌지만 어느 한쪽이 속도감 있게 진행되진 않는거 같다. 그래도 화이팅🧚🏻‍♀️

profile
Always testing, sometimes dog walking

0개의 댓글