PostgreSQL 백업/복구 Part 6 — 백업 자동화 & 모니터링, 그리고 복구 훈련
백업 파일을 만드는 것만으로는 부족하다. 자동화는 실행과 검증을 하나의 사이클로 묶어야 조용한 실패를 막고, 모니터링은 파일 존재 확인에서 복구 가능성 검증까지 5계층을 커버해야 하며, 복구 훈련은 월간 논리 복구·분기 PITR·반기 블랙아웃 드릴을 통해 RTO/RPO와 팀 역량을 측정해야 한다. 예측 가능한 시간 안에 서비스를 되살릴 수 있을 때 진짜 안정성이다.
시리즈 구성
- Part 1 — 백업의 기초와 전략
- Part 2 — 논리적 백업 — pg_dump & pg_dumpall 실전 활용
- Part 3 — 물리적 백업 — pg_basebackup과 WAL 아카이빙
- Part 4 — PITR(Point-in-Time Recovery) 구현 가이드
- Part 5 — 엔터프라이즈 백업 도구 비교 — pgBackRest vs Barman vs WAL-G
- Part 6 — 백업 자동화 & 모니터링, 그리고 복구 훈련 (현재 편 · 시리즈 완결)
목차
- 들어가며 — 전략을 완성하는 마지막 세 기둥
- 백업 자동화 — 사람이 기억에 의존해서는 안 된다
- 백업 모니터링 — 조용한 실패를 잡아내는 체계
- 복구 훈련 — 재해는 준비된 자에게만 예측 가능하다
- 복구 절차서(Runbook) — 새벽 3시에도 따라할 수 있게
- 백업 전략 성숙도 자가 점검표
- 시리즈를 마치며 — 복구 가능성이 곧 안정성이다
1. 들어가며 — 전략을 완성하는 마지막 세 기둥
파트 1부터 5까지 우리는 백업의 개념, 논리·물리 백업 도구, PITR 구현, 엔터프라이즈 도구를 두루 살펴봤다. 이제 그 모든 것을 살아 숨쉬는 운영 체계로 만드는 마지막 세 가지 요소를 다룬다.
① 자동화 — 사람의 손을 최대한 배제해 실수와 누락을 없앤다
② 모니터링 — 조용한 실패를 조기에 발견한다
③ 복구 훈련 — 실제 재해 상황에서 팀이 흔들리지 않게 만든다
이 세 가지가 빠진 백업 전략은 마치 소화기를 사두고 위치를 아무도 모르는 사무실과 같다.
"조직의 절반만이 데이터베이스 재해 복구 계획을 갖고 있다. 그 중 50%는 심각한 데이터 손실을 경험한 후에야 계획을 수립했다." — Barman / EnterpriseDB
2. 백업 자동화 — 사람이 기억에 의존해서는 안 된다
2.1 자동화의 원칙
백업 자동화에서 가장 중요한 원칙은 하나다.
백업 실행과 백업 검증은 반드시 세트로 자동화한다.
실행만 자동화하고 검증을 수동으로 두면, 조용히 실패한 백업이 몇 주 동안 방치될 수 있다. 또한 자동화 스크립트에는 반드시 실패 시 알림이 포함되어야 한다.
2.2 종합 백업 자동화 스크립트
아래는 물리 백업(pgBackRest), 논리 백업(pg_dump), WAL 아카이빙 상태 점검을 하나로 묶은 프로덕션용 자동화 스크립트다.
#!/bin/bash
# /usr/local/bin/pg_backup_full_cycle.sh
# 물리 + 논리 백업 자동화 + 검증 + 알림
set -euo pipefail
# === 설정 ======================================================
DB_HOST="localhost"
DB_USER="postgres"
BACKUP_ROOT="/backups"
LOG_FILE="/var/log/pg_backup/backup_$(date +%Y%m%d).log"
ALERT_EMAIL="dba-team@example.com"
SLACK_WEBHOOK="${PG_SLACK_WEBHOOK}" # 보안: Webhook URL은 환경 변수로 관리
RETENTION_DAYS=30
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# === 함수 정의 ==================================================
log() {
local level="$1"; shift
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${level}] $*" | tee -a "${LOG_FILE}"
}
send_alert() {
local subject="$1"
local message="$2"
# 이메일 알림
echo "${message}" | mail -s "[PG BACKUP ALERT] ${subject}" "${ALERT_EMAIL}" 2>/dev/null || true
# Slack 알림
curl -s -X POST "${SLACK_WEBHOOK}" \
-H 'Content-type: application/json' \
--data "{\"text\":\"[PG BACKUP ALERT]\n*${subject}*\n${message}\"}" \
2>/dev/null || true
log "ALERT" "알림 발송: ${subject}"
}
send_success() {
local message="$1"
curl -s -X POST "${SLACK_WEBHOOK}" \
-H 'Content-type: application/json' \
--data "{\"text\":\"[PG BACKUP OK]\n${message}\"}" \
2>/dev/null || true
}
check_wal_archiving() {
log "INFO" "WAL 아카이빙 상태 점검 중..."
local failed_count
failed_count=$(psql -U "${DB_USER}" -h "${DB_HOST}" -tAc \
"SELECT failed_count FROM pg_stat_archiver;")
if [ "${failed_count}" -gt 0 ]; then
local last_failed
last_failed=$(psql -U "${DB_USER}" -h "${DB_HOST}" -tAc \
"SELECT last_failed_wal || ' at ' || last_failed_time FROM pg_stat_archiver;")
send_alert "WAL 아카이빙 실패" "실패 횟수: ${failed_count}\n마지막 실패: ${last_failed}"
return 1
fi
local lag_seconds
lag_seconds=$(psql -U "${DB_USER}" -h "${DB_HOST}" -tAc \
"SELECT EXTRACT(EPOCH FROM (now() - last_archived_time))::int FROM pg_stat_archiver;")
# 아카이빙 지연 30분 초과 시 경보
if [ "${lag_seconds:-0}" -gt 1800 ]; then
send_alert "WAL 아카이빙 지연" \
"마지막 아카이빙으로부터 ${lag_seconds}초 경과 (임계값: 1800초)"
return 1
fi
log "INFO" "WAL 아카이빙 정상 (마지막 아카이빙 ${lag_seconds:-?}초 전)"
}
run_physical_backup() {
log "INFO" "물리적 백업(pgBackRest) 시작..."
local backup_type="${1:-diff}" # full / diff / incr
if sudo -u postgres pgbackrest \
--stanza=main \
--type="${backup_type}" \
--log-level-console=warn \
backup >> "${LOG_FILE}" 2>&1; then
log "INFO" "물리적 백업(${backup_type}) 완료"
return 0
else
send_alert "물리적 백업 실패" \
"pgBackRest ${backup_type} 백업 실패\n로그: ${LOG_FILE}"
return 1
fi
}
run_logical_backup() {
log "INFO" "논리적 백업(pg_dump) 시작..."
local db_list
db_list=$(psql -U "${DB_USER}" -h "${DB_HOST}" -tAc \
"SELECT datname FROM pg_database WHERE datistemplate = false AND datname != 'postgres';")
local failed=0
for db in ${db_list}; do
local dump_file="${BACKUP_ROOT}/logical/${db}_${TIMESTAMP}.dump"
mkdir -p "${BACKUP_ROOT}/logical"
if pg_dump -Fc -U "${DB_USER}" -h "${DB_HOST}" -d "${db}" \
-f "${dump_file}" >> "${LOG_FILE}" 2>&1; then
local size
size=$(du -sh "${dump_file}" | cut -f1)
log "INFO" "논리적 백업 완료: ${db} (${size})"
else
send_alert "논리적 백업 실패" "데이터베이스: ${db}\n파일: ${dump_file}"
failed=$((failed + 1))
fi
done
return ${failed}
}
cleanup_old_backups() {
log "INFO" "만료 백업 정리 중 (보관: ${RETENTION_DAYS}일)..."
local deleted
deleted=$(find "${BACKUP_ROOT}/logical" -name "*.dump" \
-mtime +${RETENTION_DAYS} -delete -print 2>/dev/null | wc -l)
log "INFO" "논리적 백업 ${deleted}개 삭제"
}
# === 메인 실행 ==================================================
mkdir -p "$(dirname "${LOG_FILE}")" "${BACKUP_ROOT}/logical"
log "INFO" "========== 백업 사이클 시작 =========="
# 1. WAL 아카이빙 상태 점검
check_wal_archiving || log "WARN" "WAL 아카이빙 이상 감지 (계속 진행)"
# 2. 물리적 백업 (요일에 따라 full/diff 자동 선택)
DOW=$(date +%u) # 1=월 ~ 7=일
if [ "${DOW}" -eq 7 ]; then
run_physical_backup "full"
else
run_physical_backup "diff"
fi
# 3. 논리적 백업
run_logical_backup
# 4. 만료 백업 정리
cleanup_old_backups
# 5. 완료 알림
ELAPSED=$(( SECONDS ))
send_success "백업 완료 — 소요: ${ELAPSED}초 | $(date '+%Y-%m-%d %H:%M:%S')"
log "INFO" "========== 백업 사이클 완료 (${ELAPSED}초) =========="
2.3 크론 스케줄 등록
# crontab -u postgres -e
# 매일 새벽 1시: 자동 백업 (일요일=전체, 평일=차등)
0 1 * * * /usr/local/bin/pg_backup_full_cycle.sh
# 매시간 정각: 증분 백업
0 * * * * pgbackrest --stanza=main --type=incr backup \
>> /var/log/pg_backup/incr_$(date +\%Y\%m\%d).log 2>&1
# 매주 일요일 새벽 12시 30분: 글로벌 객체 백업
30 0 * * 0 pg_dumpall -U postgres --globals-only \
-f /backups/logical/globals_$(date +\%Y\%m\%d).sql
3. 백업 모니터링 — 조용한 실패를 잡아내는 체계
3.1 모니터링의 5계층
효과적인 백업 모니터링은 다음 5개 계층을 모두 커버해야 한다.
Level 1: 존재 확인 — 백업 파일이 생성됐는가?
Level 2: 무결성 확인 — 파일이 손상되지 않았는가?
Level 3: 부분 복구 확인 — 특정 테이블을 추출할 수 있는가?
Level 4: 전체 복구 확인 — 데이터베이스 전체를 복구할 수 있는가?
Level 5: 애플리케이션 확인 — 복구 후 앱이 정상 동작하는가?
일상 모니터링은 Level 1-2를, 정기 검증(월 1회)은 Level 3-4를, 연간 훈련은 Level 5까지 포함해야 한다.
3.2 핵심 모니터링 쿼리
권한 요건:
pg_ls_waldir()와pg_control_checkpoint()는 superuser 또는pg_monitor역할이 필요하다. 전용 모니터링 계정에GRANT pg_monitor TO monitoring_user;를 실행하라.
-- ① WAL 아카이빙 종합 상태
SELECT
archived_count,
failed_count,
last_archived_wal,
to_char(last_archived_time, 'YYYY-MM-DD HH24:MI:SS') AS last_archived_at,
EXTRACT(EPOCH FROM (now() - last_archived_time))::int AS lag_seconds,
last_failed_wal,
to_char(last_failed_time, 'YYYY-MM-DD HH24:MI:SS') AS last_failed_at
FROM pg_stat_archiver;
-- ② pg_wal 디렉토리 누적 크기 (비정상적으로 커지면 아카이빙 적체)
SELECT
pg_size_pretty(sum(size)) AS wal_dir_size,
count(*) AS wal_file_count
FROM pg_ls_waldir();
-- ③ 마지막 체크포인트 시간 (너무 오래됐으면 문제)
SELECT
to_char(checkpoint_time, 'YYYY-MM-DD HH24:MI:SS') AS last_checkpoint,
EXTRACT(EPOCH FROM (now() - checkpoint_time))::int AS seconds_ago
FROM pg_control_checkpoint();
-- ④ 현재 복제 지연 (스탠바이가 있을 경우)
SELECT
application_name,
state,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn)) AS send_lag,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)) AS replay_lag
FROM pg_stat_replication;
3.3 자동화된 상태 점검 스크립트
#!/bin/bash
# /usr/local/bin/pg_backup_healthcheck.sh
# 크론으로 5분마다 실행
DB_USER="postgres"
SLACK_WEBHOOK="${PG_SLACK_WEBHOOK}" # 보안: Webhook URL은 환경 변수로 관리
BACKUP_ROOT="/backups"
MAX_BACKUP_AGE_HOURS=26 # 마지막 백업 허용 최대 경과 시간
MAX_WAL_LAG_MINUTES=30 # WAL 아카이빙 허용 최대 지연
alert() {
local emoji="$1"; local msg="$2"
curl -s -X POST "${SLACK_WEBHOOK}" \
-H 'Content-type: application/json' \
--data "{\"text\":\"${emoji} *[PG HEALTH]* ${msg}\"}" >/dev/null
}
# 점검 1: WAL 아카이빙 실패
FAILED=$(psql -U "${DB_USER}" -tAc \
"SELECT failed_count FROM pg_stat_archiver;")
[ "${FAILED:-0}" -gt 0 ] && \
alert "[RED]" "WAL 아카이빙 실패 ${FAILED}회 누적"
# 점검 2: WAL 아카이빙 지연
LAG=$(psql -U "${DB_USER}" -tAc \
"SELECT COALESCE(EXTRACT(EPOCH FROM now()-last_archived_time)::int, 99999)
FROM pg_stat_archiver;")
[ "${LAG:-99999}" -gt $((MAX_WAL_LAG_MINUTES * 60)) ] && \
alert "[WARN]" "WAL 아카이빙 지연 ${LAG}초 (임계값: $((MAX_WAL_LAG_MINUTES * 60))초)"
# 점검 3: pg_wal 디렉토리 파일 수 이상 급증
WAL_COUNT=$(psql -U "${DB_USER}" -tAc \
"SELECT count(*) FROM pg_ls_waldir() WHERE name ~ '^[0-9A-F]{24}$';")
[ "${WAL_COUNT:-0}" -gt 100 ] && \
alert "[WARN]" "pg_wal 파일 누적 ${WAL_COUNT}개 — 아카이빙 적체 의심"
# 점검 4: 최신 백업 파일 존재 여부 (논리 백업 기준)
LATEST_BACKUP=$(find "${BACKUP_ROOT}/logical" -name "*.dump" \
-newer "$(date -d "-${MAX_BACKUP_AGE_HOURS} hours" +%Y%m%d%H%M)" 2>/dev/null | wc -l)
[ "${LATEST_BACKUP}" -eq 0 ] && \
alert "[RED]" "최근 ${MAX_BACKUP_AGE_HOURS}시간 내 백업 파일 없음!"
# 점검 5: 디스크 사용량 (80% 초과 시 경고)
DISK_USE=$(df -h "${BACKUP_ROOT}" | awk 'NR==2{print $5}' | tr -d '%')
[ "${DISK_USE:-0}" -gt 80 ] && \
alert "[WARN]" "백업 디스크 사용률 ${DISK_USE}% (임계값: 80%)"
3.4 Prometheus + Grafana 연동
postgres_exporter를 활용하면 백업 관련 메트릭을 Prometheus로 수집하고 Grafana 대시보드에서 시각화할 수 있다.
# /etc/postgres_exporter/custom_queries.yaml
# 백업 상태 커스텀 메트릭
pg_backup_archiver:
query: |
SELECT
archived_count,
failed_count,
EXTRACT(EPOCH FROM (now() - last_archived_time)) AS last_archive_lag_seconds
FROM pg_stat_archiver;
metrics:
- archived_count:
usage: COUNTER
description: "누적 WAL 아카이빙 성공 횟수"
- failed_count:
usage: COUNTER
description: "누적 WAL 아카이빙 실패 횟수"
- last_archive_lag_seconds:
usage: GAUGE
description: "마지막 성공 아카이빙으로부터 경과 시간(초)"
pg_backup_wal_files:
query: |
SELECT count(*) AS pending_wal_files
FROM pg_ls_waldir()
WHERE name ~ '^[0-9A-F]{24}$';
metrics:
- pending_wal_files:
usage: GAUGE
description: "pg_wal 디렉토리 내 미처리 WAL 파일 수"
# Prometheus 알림 규칙 (prometheus/alerts/postgresql_backup.yml)
groups:
- name: postgresql_backup
rules:
# WAL 아카이빙 실패 즉시 알림
- alert: PostgreSQLWALArchivingFailed
expr: increase(pg_backup_archiver_failed_count[5m]) > 0
for: 0m
labels:
severity: critical
annotations:
summary: "PostgreSQL WAL 아카이빙 실패"
description: "{{ $labels.instance }}에서 WAL 아카이빙 실패 발생"
# WAL 아카이빙 30분 이상 지연
- alert: PostgreSQLWALArchiveLag
expr: pg_backup_archiver_last_archive_lag_seconds > 1800
for: 5m
labels:
severity: warning
annotations:
summary: "WAL 아카이빙 지연"
description: "마지막 아카이빙으로부터 {{ $value | humanizeDuration }} 경과"
# pg_wal 파일 100개 초과 누적
- alert: PostgreSQLWALPileup
expr: pg_backup_wal_files_pending_wal_files > 100
for: 10m
labels:
severity: warning
annotations:
summary: "pg_wal 파일 누적"
description: "현재 {{ $value }}개의 WAL 파일 누적 — 아카이빙 적체 가능성"
4. 복구 훈련 — 재해는 준비된 자에게만 예측 가능하다
4.1 왜 훈련이 필수인가
실제 장애 상황은 이런 모습이다.
새벽 3시 17분 — 온콜 알림
담당자: 반수면 상태, 노트북 찾는 중
팀원: 절반은 자고 있음
압박감: 서비스 다운 중, 경영진 대기 중
이 상황에서 처음 해보는 복구를 수행하는 것은 기술 문제가 아닌 인적 위기다. 훈련은 이 상황을 "이미 해봤던 일"로 바꾼다.
"백업 도구는 전략의 실행 수단이다. 팀이 훈련되지 않으면 아무리 훌륭한 도구도 소용없다."
4.2 복구 훈련 5단계 프레임워크
Level 1: 존재 확인 드릴 — 매주 자동 (스크립트)
Level 2: 무결성 검증 드릴 — 매주 자동 (pg_verifybackup)
Level 3: 논리적 선택 복구 — 매월 수동 (특정 테이블 복구)
Level 4: 전체 PITR 복구 훈련 — 분기 수동 (별도 환경, 전체 시나리오)
Level 5: 블랙아웃 드릴 — 반기 1회 (사전 예고 없이, 팀 전체 참여)
4.3 월간 논리 복구 훈련 스크립트 (Level 3)
#!/bin/bash
# /usr/local/bin/pg_restore_drill_monthly.sh
# 매월 1일 새벽 3시 실행 (cron: 0 3 1 * *)
set -euo pipefail
BACKUP_ROOT="/backups/logical"
TEST_DB="restore_drill_$(date +%Y%m)"
PROD_DB="production"
DB_USER="postgres"
LOG_FILE="/var/log/pg_backup/drill_$(date +%Y%m%d).log"
SLACK_WEBHOOK="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "${LOG_FILE}"; }
notify() {
curl -s -X POST "${SLACK_WEBHOOK}" \
-H 'Content-type: application/json' \
--data "{\"text\":\"$1\"}" >/dev/null
}
log "===== 월간 복구 훈련 시작 ====="
START_TIME=${SECONDS}
# 1. 최신 덤프 파일 찾기
LATEST_DUMP=$(find "${BACKUP_ROOT}" -name "${PROD_DB}_*.dump" \
-printf '%T@ %p\n' | sort -n | tail -1 | cut -d' ' -f2)
if [ -z "${LATEST_DUMP}" ]; then
notify "복구 훈련 실패: 백업 파일을 찾을 수 없음"
exit 1
fi
log "복구 대상 백업: ${LATEST_DUMP}"
# 2. 무결성 검증
DUMP_SIZE=$(du -sh "${LATEST_DUMP}" | cut -f1)
if ! pg_restore --list "${LATEST_DUMP}" > /dev/null 2>&1; then
notify "복구 훈련 실패: 백업 파일 손상 (${LATEST_DUMP})"
exit 1
fi
log "무결성 검증 통과 (크기: ${DUMP_SIZE})"
# 3. 테스트 DB 생성 및 복구
psql -U "${DB_USER}" -c "DROP DATABASE IF EXISTS ${TEST_DB};" > /dev/null
psql -U "${DB_USER}" -c "CREATE DATABASE ${TEST_DB};" > /dev/null
pg_restore -U "${DB_USER}" -d "${TEST_DB}" -j 4 "${LATEST_DUMP}" \
>> "${LOG_FILE}" 2>&1
log "복구 완료 -> ${TEST_DB}"
# 4. 데이터 검증 쿼리
PROD_COUNT=$(psql -U "${DB_USER}" -d "${PROD_DB}" -tAc \
"SELECT count(*) FROM pg_tables WHERE schemaname='public';")
TEST_COUNT=$(psql -U "${DB_USER}" -d "${TEST_DB}" -tAc \
"SELECT count(*) FROM pg_tables WHERE schemaname='public';")
if [ "${PROD_COUNT}" -ne "${TEST_COUNT}" ]; then
notify "복구 훈련 경고: 테이블 수 불일치 (원본: ${PROD_COUNT}, 복구: ${TEST_COUNT})"
else
log "테이블 수 일치: ${TEST_COUNT}개"
fi
# 5. 테스트 DB 정리
psql -U "${DB_USER}" -c "DROP DATABASE ${TEST_DB};" > /dev/null
# 6. 결과 보고
ELAPSED=$((SECONDS - START_TIME))
notify "월간 복구 훈련 완료 | 백업: $(basename ${LATEST_DUMP}) | 크기: ${DUMP_SIZE} | 소요: ${ELAPSED}초 | 테이블: ${TEST_COUNT}개"
log "===== 훈련 완료 (${ELAPSED}초) ====="
4.4 분기 PITR 전체 복구 훈련 가이드 (Level 4)
분기별 PITR 훈련은 반드시 별도 환경에서 수행한다. 훈련 중 프로덕션에 영향을 주어선 안 된다.
훈련 시나리오 예시: "오늘 오후 2시 30분에 중요 테이블이 DROP됐다. 2시 29분 상태로 복구하라."
# 분기 PITR 훈련 절차 (별도 서버에서 실행)
# [훈련 진행자] 복구 목표 시각 부여 (참가자에게는 사전 미공개)
TARGET_TIME="2026-04-14 14:29:59 Asia/Seoul"
# Step 1: 베이스 백업 복원
mkdir -p /drill/datadir
tar -xzf /backups/basebackup/latest/base.tar.gz -C /drill/datadir
# Step 2: recovery.signal 생성 및 복구 파라미터 설정
touch /drill/datadir/recovery.signal
cat >> /drill/datadir/postgresql.auto.conf << EOF
restore_command = 'cp /var/lib/postgresql/archive/%f %p'
recovery_target_time = '${TARGET_TIME}'
recovery_target_action = 'pause'
port = 5555
EOF
# Step 3: PostgreSQL 시작 (복구 모드)
pg_ctl -D /drill/datadir start
# Step 4: 복구 진행 모니터링 (훈련 참가자 직접 수행)
tail -f /drill/datadir/log/postgresql-*.log
# Step 5: 복구 완료 확인 및 데이터 검증 (훈련 참가자 직접 수행)
psql -p 5555 -U postgres -c "SELECT pg_is_in_recovery();"
psql -p 5555 -U postgres -c "SELECT COUNT(*) FROM critical_table;"
psql -p 5555 -U postgres -c "SELECT MAX(created_at) FROM orders;"
# Step 6: 운영 모드 전환
psql -p 5555 -U postgres -c "SELECT pg_wal_replay_resume();"
# Step 7: 훈련 종료 및 정리
pg_ctl -D /drill/datadir stop
rm -rf /drill/datadir
4.5 훈련 결과 측정 지표
훈련 후 반드시 다음 지표를 기록하고 추이를 관리한다.
| 지표 | 측정 방법 | 목표 |
|---|---|---|
| 복구 소요 시간(RTO) | 훈련 시작 ~ 서비스 재개까지 | 목표 RTO 이내 |
| 데이터 손실 범위(RPO) | 복구 지점 ~ 장애 발생 시각 (pg_stat_archiver의 last_archived_time과 장애 시각 대조) | 목표 RPO 이내 |
| 절차 성공률 | 오류 없이 완료한 단계 / 전체 단계 | 100% |
| 팀원 참여율 | 훈련 참여 인원 / 전체 온콜 인원 | 100% |
| 절차서 정확도 | 실제 수행과 문서의 일치율 | 100% |
5. 복구 절차서(Runbook) — 새벽 3시에도 따라할 수 있게
모든 복구 절차는 팀에서 가장 경험이 적은 사람도 따라할 수 있을 정도로 문서화돼야 한다.
5.1 필수 Runbook 구성 요소
# [DB 복구 Runbook] 테이블 DROP 사고
## 사전 조건 확인
- [ ] 마지막 백업 위치: /backups/basebackup/LATEST
- [ ] WAL 아카이브 위치: /var/lib/postgresql/archive/
- [ ] 복구 서버 접속 정보: restore-server.internal
- [ ] 비상 연락망: dba-oncall@example.com / Slack #db-alerts
## 복구 결정 기준
- RPO: 5분 (WAL 아카이빙 주기)
- RTO: 2시간 (전체 복구 기준)
## 복구 단계 (PITR)
1. [ ] 서비스 트래픽 차단 (또는 DB 접속 차단)
2. [ ] 사고 발생 시각 정확히 확인 (로그/알림 시각)
3. [ ] 복구 목표 시각 결정 (사고 시각 - 1분)
4. [ ] 베이스 백업 확인 및 복원
5. [ ] recovery.signal + postgresql.auto.conf 설정
6. [ ] PostgreSQL 시작 및 복구 진행 모니터링
7. [ ] 데이터 검증 (행 수, 최신 레코드 확인)
8. [ ] pg_wal_replay_resume() 실행
9. [ ] 시퀀스 재설정 확인
10. [ ] 서비스 트래픽 복구
11. [ ] 즉시 새 베이스 백업 실행
12. [ ] 사후 보고서 작성
## 에스컬레이션
- 30분 내 복구 시작 불가 -> 팀장 연락
- 1시간 내 복구 전망 불확실 -> CTO 보고
5.2 Runbook 유지 관리 원칙
[ ] 매 분기 복구 훈련 후 문서 업데이트
[ ] PostgreSQL 버전 업그레이드 시 재검증
[ ] 도구 버전 변경(pgBackRest, Barman 등) 시 명령어 재확인
[ ] 팀원 변경 시 새 구성원과 함께 드라이런 실시
[ ] 문서는 Git으로 버전 관리 (변경 이력 추적)
6. 백업 전략 성숙도 자가 점검표
지금까지 배운 모든 내용을 바탕으로 현재 조직의 백업 전략 성숙도를 점검해보자.
기초 (Foundation)
[ ] pg_dump 또는 pg_basebackup으로 정기 백업이 자동화됐다
[ ] 백업 파일이 프로덕션 서버와 분리된 위치에 저장된다
[ ] 백업 실패 시 알림이 발송된다
[ ] 보관 정책(retention policy)이 설정돼 있다
중급 (Intermediate)
[ ] WAL 아카이빙이 설정되어 PITR이 가능하다
[ ] pg_verifybackup 또는 동등한 검증이 자동화됐다
[ ] 월 1회 이상 실제 복구 테스트를 수행한다
[ ] 백업 모니터링이 Prometheus/Grafana 등으로 시각화됐다
[ ] 3-2-1 원칙(3개 복사본, 2가지 미디어, 1개 오프사이트)이 적용됐다
고급 (Advanced)
[ ] pgBackRest / Barman / WAL-G 등 전문 도구가 운영 중이다
[ ] 블록 수준 증분 백업으로 스토리지와 시간이 최적화됐다
[ ] 분기 1회 이상 전체 PITR 훈련을 수행하고 결과를 기록한다
[ ] 복구 절차서(Runbook)가 최신화돼 있고 팀 전원이 숙지하고 있다
[ ] RTO/RPO 목표치가 문서화되고 정기적으로 검증된다
[ ] 중요 작업 전 pg_create_restore_point()가 습관화됐다
[ ] 백업 메트릭 추이(크기, 소요 시간, 성공률)를 추적하고 있다
7. 시리즈를 마치며 — 복구 가능성이 곧 안정성이다
6개 파트에 걸친 긴 여정이었다. 처음 Part 1에서 던진 질문으로 돌아가 보자.
"백업이 있다"는 것과 "언제든 복구할 수 있다"는 것은 같은 말인가?
이제 그 대답을 명확히 할 수 있다 — 같지 않다.
백업은 파일이 생기는 것으로 끝나지 않는다. 그 파일이 손상되지 않았음을 검증하고, 팀이 복구 절차를 숙지하고, 실제로 복구에 성공하는 연습을 반복해야 비로소 "복구 가능한 상태"가 된다.
파일이 있다 ≠ 백업이 있다
백업이 있다 ≠ 복구할 수 있다
복구할 수 있다 ≠ 빠르게 복구할 수 있다
빠르게 복구할 수 있다 = 진짜 안정성
이 시리즈에서 다룬 내용을 한 문장으로 압축한다면:
"PostgreSQL 백업의 목표는 파일을 만드는 것이 아니라, 어떤 재해에도 예측 가능한 시간 안에 서비스를 되살리는 것이다."
지금 당장 팀의 백업 전략을 점검하고, 가장 취약한 한 가지부터 개선하기 바란다.
시리즈 전체 요약
| 파트 | 제목 | 핵심 내용 |
|---|---|---|
| Part 1 | 백업의 기초 | RPO/RTO, 3가지 백업 방식, 3-2-1 원칙, 흔한 실수 |
| Part 2 | 논리적 백업 | pg_dump 포맷, 병렬 덤프/복구, pg_dumpall, 자동화 |
| Part 3 | 물리적 백업 | pg_basebackup, WAL 아카이빙, 증분 백업(v17+) |
| Part 4 | PITR 구현 | 복구 목표 파라미터, 단계별 복구, 타임라인, 시나리오 |
| Part 5 | 도구 비교 | pgBackRest vs Barman vs WAL-G 심층 비교 |
| Part 6 | 자동화 & 훈련 | 자동화 스크립트, 모니터링, Runbook, 성숙도 점검 |
참고 자료
공식 문서
- PostgreSQL 18 Documentation — Backup and Restore
- PostgreSQL 18 Documentation — Continuous Archiving and PITR
- pgBackRest User Guide
- Barman Documentation v3.18
- WAL-G GitHub
2025-2026 주요 아티클
- 13 PostgreSQL Backup Best Practices — Medium (Nov 2025)
- PostgreSQL Backup Verification — DEV Community (Jan 2026)
- Automating Backups and DR: pgBackRest vs Barman — Severalnines (Nov 2025)
- PostgreSQL Disaster Recovery — Stormatics (Feb 2026)
- Best PostgreSQL Backup Solutions in 2026 — PostgresGUI (Feb 2026)
- How to Test PostgreSQL Backup Restoration — OneUptime (Jan 2026)