가이드 / DB 백업 검증

DB 백업이 진짜 됐는지 검증하는 법

백업 스크립트가 에러 없이 끝났다는 것과 복구할 수 있다는 것은 완전히 다른 이야기입니다. 실제로 사고는 대부분 이런 식으로 납니다:

공통점은 사고가 난 날에야 알게 된다는 것입니다. 아래 5단계를 자동화해두면 그날이 오기 전에 알 수 있습니다.

1단계. 파일이 실제로 생겼는가

가장 기본이지만 빠뜨리기 쉽습니다. set -e를 넣어 중간에 실패하면 즉시 멈추게 하고, 파이프 중간 실패도 잡히도록 set -o pipefail을 함께 씁니다.

pg_dump는 파이프 앞단에서 실패해도 gzip이 성공하면 종료코드 0
#!/bin/bash
set -euo pipefail   # pipefail이 없으면 pg_dump 실패를 놓친다

OUT=~/backups/db-$(date +%F).sql.gz
pg_dump -U myuser mydb | gzip > "$OUT"
test -s "$OUT"      # 0바이트면 여기서 실패

2단계. 크기가 갑자기 변하지 않았는가

데이터가 쌓이는 서비스라면 백업 크기는 완만하게 커집니다. 어제보다 절반 이하로 작아졌다면 뭔가 잘못된 겁니다. 실제 저희 백업 크기 추이를 보면:

ls -la ~/backups/
3,765   uptime-2026-08-03.sql.gz
37,671  uptime-2026-08-04.sql.gz
61,594  uptime-2026-08-05.sql.gz
89,539  uptime-2026-08-06.sql.gz   ← 꾸준히 증가 = 정상

어제 파일과 비교해 50% 미만이면 경고를 내도록 해두면, 빈 덤프·잘린 파일을 그날 잡습니다.

직전 백업 대비 크기 검사
PREV=$(ls -t ~/backups/*.sql.gz | sed -n 2p)
if [ -n "$PREV" ]; then
  NEW_SIZE=$(stat -c%s "$OUT")
  OLD_SIZE=$(stat -c%s "$PREV")
  if [ "$NEW_SIZE" -lt $((OLD_SIZE / 2)) ]; then
    echo "백업 크기 급감: $OLD_SIZE → $NEW_SIZE" >&2
    exit 1
  fi
fi

3단계. 압축 파일이 깨지지 않았는가

디스크가 차거나 프로세스가 죽으면 파일은 남지만 내용이 잘립니다. gzip은 자체 검사가 가능하니 매번 검사하세요. 수 초면 끝납니다.

무결성 검사
gzip -t "$OUT"   # 깨졌으면 여기서 실패

# 덤프 끝부분이 정상적으로 닫혔는지까지 확인 (pg_dump는 마지막에 이 줄을 남긴다)
zcat "$OUT" | tail -5 | grep -q "PostgreSQL database dump complete"

4단계. 진짜로 복구되는가 (핵심)

여기까지 통과해도 복구는 별개 문제입니다. 확장 기능이 없거나 권한이 달라 복구가 실패하는 경우가 흔합니다. 확실한 방법은 하나뿐 — 임시 DB에 실제로 넣어보는 것. 주 1회 정도면 충분합니다.

복구 리허설 (임시 DB에 넣고 검증 후 삭제)
RESTORE_DB="restore_check_$(date +%s)"

createdb -U myuser "$RESTORE_DB"
zcat "$OUT" | psql -U myuser -d "$RESTORE_DB" -v ON_ERROR_STOP=1 -q

# 핵심 테이블의 행 수가 원본과 비슷한지 확인 (0이면 사고)
ROWS=$(psql -U myuser -d "$RESTORE_DB" -tAc 'SELECT count(*) FROM "User"')
echo "복구된 사용자 수: $ROWS"
[ "$ROWS" -gt 0 ] || { echo "복구본이 비었다" >&2; exit 1; }

dropdb -U myuser "$RESTORE_DB"

ON_ERROR_STOP=1이 중요합니다. 없으면 psql이 오류를 뿜으면서도 끝까지 진행하고 종료 코드 0을 돌려줘서, 실패한 복구를 성공으로 착각하게 됩니다.

5단계. 스크립트가 그날 돌긴 했는가

1~4단계를 아무리 잘 만들어도 스크립트 자체가 안 돌면 전부 무의미합니다. 그리고 이건 서버 안에서는 알 수 없습니다 — 안 돈 일은 로그도 안 남으니까요.

그래서 마지막 줄에 외부로 신호를 보냅니다. 검증까지 모두 통과했을 때만 호출되므로,“오늘 백업이 검증까지 끝났다”는 뜻이 됩니다:

검증 성공 시에만 신호 전송
curl -fsS --retry 3 --retry-connrefused \
  --connect-timeout 5 --max-time 15 \
  https://batch.hkch.co.kr/p/YOUR_TOKEN

예정 시각까지 이 신호가 안 오면 디스코드·슬랙·텔레그램으로 알림이 갑니다. 스크립트가 실패해서든, 서버가 꺼져서든, 크론이 안 돌아서든 결과는 하나 — 백업이 없다는 것이고, 그걸 그날 알게 됩니다.

실제로 저희가 당한 사고 두 건

① 백업이 9시간 어긋나 돌고 있었다.클라우드 서버가 UTC라 “새벽 4시 백업”이 실제로는 한국 시간 오후 1시에 돌고 있었습니다. 트래픽이 있는 시간대에 덤프를 뜨고 있었던 셈이죠. 모니터링 알림으로 발견했습니다.

② 고쳤다고 믿은 수정이 사실 안 먹혔다. 위 문제를 CRON_TZ=Asia/Seoul로 고쳤는데 — 우분투 기본 cron은 CRON_TZ를 지원하지 않습니다. 조용히 무시돼서 여전히 UTC로 돌고 있었고, 이번엔 예정 시각에 신호가 안 와서 알림이 왔습니다. 결국 시각을 UTC로 환산해(0 19 * * * = 한국 04:00) 해결했습니다.

두 번 다 서버 로그만 봤으면 몰랐을 일입니다. 자세한 시간대 함정은 crontab이 안 돌아갈 때 체크리스트 8번을 참고하세요.

전체 스크립트

~/bin/backup-verify.sh
#!/bin/bash
set -euo pipefail

DB=mydb
USER=myuser
OUT=~/backups/db-$(date +%F).sql.gz

# 1. 백업
pg_dump -U "$USER" "$DB" | gzip > "$OUT"
test -s "$OUT"

# 2. 크기 급감 검사
PREV=$(ls -t ~/backups/*.sql.gz | sed -n 2p || true)
if [ -n "${PREV:-}" ]; then
  [ "$(stat -c%s "$OUT")" -ge $(( $(stat -c%s "$PREV") / 2 )) ] \
    || { echo "백업 크기 급감" >&2; exit 1; }
fi

# 3. 무결성
gzip -t "$OUT"
zcat "$OUT" | tail -5 | grep -q "PostgreSQL database dump complete"

# 4. 복구 리허설 (주 1회만: 일요일)
if [ "$(date +%u)" = "7" ]; then
  T="restore_check_$(date +%s)"
  createdb -U "$USER" "$T"
  zcat "$OUT" | psql -U "$USER" -d "$T" -v ON_ERROR_STOP=1 -q
  dropdb -U "$USER" "$T"
fi

# 5. 오래된 백업 정리 후, 여기까지 왔을 때만 신호
find ~/backups -name "*.sql.gz" -mtime +14 -delete
curl -fsS --retry 3 --retry-connrefused --connect-timeout 5 --max-time 15 \
  https://batch.hkch.co.kr/p/YOUR_TOKEN

crontab 등록 (서버가 UTC라면 한국 새벽 4시는 0 19):

crontab -e
0 19 * * * ~/bin/backup-verify.sh >> ~/logs/backup.log 2>&1

표현식이 헷갈린다면 cron 표현식 검사기에서 다음 실행 시각을 먼저 확인해 보세요.

정리

백업이 안 된 날, 그날 바로 알기

구글 계정으로 3초 가입 · 모니터 3개 무료 · 카드 등록 없음

무료로 시작하기 →