서비스 운영 중 갑자기 DB 서버 디스크가 가득 차서 MySQL이 멈췄습니다. 백업이 있다고 생각했는데 담당자가 퇴사하면서 백업 스크립트가 실제로 돌아가고 있는지 아무도 모르는 상태입니다. 복구를 시도하려 해도 마지막 백업이 3개월 전 것뿐입니다.
백업은 "있으면 좋은 것"이 아닙니다. 없으면 복구 자체가 불가능합니다. 이 모듈에서는 파일·설정·DB를 어떻게 백업하고, 실제로 복구가 되는지 검증하는 방법까지 다룹니다.
- 1RTO와 RPO 개념을 설명하고 우리 서비스 기준을 정의할 수 있다
- 2Full / Incremental / Differential 백업 차이를 알고 전략을 선택할 수 있다
- 3tar + rsync로 파일/설정 백업 스크립트를 작성하고 실행할 수 있다
- 4mysqldump --single-transaction으로 무중단 DB 백업을 수행할 수 있다
- 5백업에서 실제 복구 테스트를 수행하고 복구 성공을 검증할 수 있다
- 6DR(재해복구) 개념과 Active-Passive / Active-Active 구성 차이를 설명할 수 있다
백업의 기본 개념
RTO, RPO, 그리고 백업 유형
서버 디스크가 고장났습니다. "백업 있죠?"라는 질문에 "있긴 한데 일주일 전 것입니다"라고 답했습니다. 1주일치 주문 데이터가 사라질 수 있는 상황입니다. 복구까지 몇 시간이 걸리냐는 질문에는 답을 못 했습니다. 백업 주기와 복구 목표를 숫자로 정해두지 않으면, 막상 사고가 났을 때 "충분한가"를 판단할 기준이 없습니다. RTO와 RPO가 그 기준입니다.
백업 전략을 세우기 전에 두 가지 숫자를 먼저 정해야 합니다. RTO와 RPO입니다. 이 숫자를 정하지 않으면 "얼마나 자주", "어떤 방식으로" 백업해야 하는지 결정할 수 없습니다.

RTO (Recovery Time Objective)
장애 발생 후 서비스 복구까지 허용되는 최대 시간
예: "RTO 4시간" → 장애 후 4시간 이내 서비스 재개 필요
RPO (Recovery Point Objective)
복구 시 허용 가능한 데이터 손실 기간
예: "RPO 1시간" → 최대 1시간 치 데이터 손실 감수 가능
→ 1시간마다 백업이 필요하다는 의미
백업 유형 비교:
| 유형 | 내용 | 용량 | 복구 방법 | 적합한 상황 |
|---|---|---|---|---|
| Full (전체) | 전체 데이터 | 크다 | Full 하나로 복구 | 주 1회 |
| Incremental (증분) | 마지막 백업 이후 변경분 | 매우 작다 | Full + 모든 Incremental 순서대로 | 매일 백업, 용량 제한 시 |
| Differential (차등) | 마지막 Full 이후 변경분 | 중간 | Full + 최신 Differential | 복구 단순성 vs 용량 균형 |
실무에서 가장 많이 쓰는 조합:
주 1회 Full 백업 (일요일 새벽)
일별 Incremental 백업 (월~토 새벽)
월별 Full 백업 → 별도 장기 보관
보관 정책 예시:
일별 백업 → 7일 보관
주별 백업 → 4주 보관
월별 백업 → 12개월 보관
파일/설정 백업
tar과 rsync로 설정 파일 백업하기
서버 장애 후 재구성할 때 가장 먼저 필요한 것이 설정 파일입니다. /etc/nginx/, /opt/tomcat/conf/, /etc/ssh/sshd_config — 이것들이 없으면 서버를 새로 만들어도 서비스가 올라오지 않습니다.
# 설정 파일 tar 백업 (날짜 포함 파일명)
sudo tar -czf /backup/config_$(date +%Y%m%d).tar.gz \
/etc/nginx/ \
/opt/tomcat/conf/ \
/etc/ssh/sshd_config \
/etc/systemd/system/
# 백업 파일 확인
ls -lh /backup/config_*.tar.gz
# 백업 내용 목록 확인 (복구 전 검증)
tar -tzf /backup/config_20260530.tar.gz | head -20
rsync로 원격 서버에 백업 (권장):
# 원격 서버로 애플리케이션 디렉터리 동기화
# --delete: 원본에서 삭제된 파일을 대상에서도 삭제
# -avz: archive(권한/타임스탬프 유지) + verbose + gzip 압축
rsync -avz /opt/app/ backup-server:/backup/app/ --delete
# 설정 파일 원격 백업
rsync -avz /etc/nginx/ backup-server:/backup/nginx-conf/
# SSH 키 인증 사용 (비밀번호 없이 자동 실행)
rsync -avz -e "ssh -i /root/.ssh/backup_key" /opt/app/ backup-server:/backup/app/
백업 자동화 (crontab):
# crontab -e 로 추가
# 매일 새벽 2시 설정 파일 백업 + rsync
0 2 * * * tar -czf /backup/config_$(date +\%Y\%m\%d).tar.gz /etc/nginx/ /opt/tomcat/conf/ && rsync -avz /backup/ backup-server:/backup/$(hostname)/
# 7일 지난 로컬 백업 삭제 (용량 관리)
0 3 * * * find /backup/ -name "config_*.tar.gz" -mtime +7 -delete
DB 백업
MySQL 무중단 백업 — mysqldump
서비스를 내리고 MySQL을 백업하라는 지시를 받았습니다. 30분이면 된다고 했는데, 새벽 트래픽이 있어 서비스 중단이 불가능합니다. 그렇다고 서비스가 살아있는 상태에서 백업하면 트랜잭션 도중에 파일이 잘려 일관성이 깨질 수 있습니다. --single-transaction 옵션이 이 문제를 해결합니다. InnoDB 스토리지 엔진의 특성을 이용해 잠금 없이 일관된 스냅샷을 만들어 줍니다.
DB 백업은 파일 백업보다 까다롭습니다. 서비스가 운영 중인 상태에서 데이터 일관성을 유지하며 백업해야 하기 때문입니다. --single-transaction이 그 핵심 옵션입니다.
# MySQL 전체 DB 무중단 백업 (InnoDB 기준)
mysqldump \
--single-transaction \
--all-databases \
--routines \
--triggers \
-u root -p \
> /backup/mysql_$(date +%Y%m%d_%H%M).sql
# 특정 DB만 백업
mysqldump --single-transaction -u root -p mydb > /backup/mydb_$(date +%Y%m%d).sql
# 압축 저장 (용량 절감)
mysqldump --single-transaction --all-databases -u root -p | gzip > /backup/mysql_$(date +%Y%m%d).sql.gz
# 백업 파일 크기 확인
ls -lh /backup/mysql_*.sql.gz
DB 복구:
# SQL 파일에서 복구
mysql -u root -p < /backup/mysql_20260530.sql
# 압축 파일에서 복구
gunzip -c /backup/mysql_20260530.sql.gz | mysql -u root -p
# 특정 DB만 복구
mysql -u root -p mydb < /backup/mydb_20260530.sql
# 복구 후 확인
mysql -u root -p -e "SHOW DATABASES;"
mysql -u root -p mydb -e "SHOW TABLES;"
PostgreSQL 백업:
# pg_dump (PostgreSQL)
pg_dump -U postgres mydb > /backup/postgres_mydb_$(date +%Y%m%d).sql
# 전체 클러스터 백업
pg_dumpall -U postgres > /backup/postgres_all_$(date +%Y%m%d).sql
# 복구
psql -U postgres mydb < /backup/postgres_mydb_20260530.sql
스냅샷 백업
VM 스냅샷과 LVM 스냅샷
OS 업그레이드 작업을 시작했는데 중간에 패키지 충돌로 시스템이 불안정해졌습니다. tar 백업을 복원하려면 OS를 재설치하고 파일을 하나씩 복사해야 합니다. 작업 전에 VM 스냅샷이 있었다면, 클릭 한 번에 작업 이전 상태로 돌아올 수 있습니다. 스냅샷은 파일 백업과 다르게 디스크 전체 상태를 특정 시점에 동결합니다. 시스템 변경 작업 전에 반드시 찍어야 하는 이유입니다.
스냅샷은 특정 시점의 디스크 상태를 통째로 보존합니다. 파일 단위가 아닌 블록 레벨 복사라 복구가 빠르지만, 공간 효율은 낮습니다.
# LVM 스냅샷 (Linux 볼륨 관리자)
# 현재 LVM 구성 확인
lvdisplay
vgdisplay
# 스냅샷 생성 (10GB 용량 할당)
sudo lvcreate -L 10G -s -n data_snap /dev/vg0/data
# 스냅샷을 마운트해서 파일 확인
sudo mkdir /mnt/snap
sudo mount /dev/vg0/data_snap /mnt/snap
ls /mnt/snap/
# 스냅샷 삭제 (더 이상 필요 없을 때)
sudo umount /mnt/snap
sudo lvremove /dev/vg0/data_snap
AWS EBS 스냅샷 (클라우드 환경):
# EBS 스냅샷 생성 (AWS CLI)
aws ec2 create-snapshot \
--volume-id vol-12345678 \
--description "Daily backup $(date +%Y%m%d)"
# 스냅샷 목록 확인
aws ec2 describe-snapshots --owner-ids self
# 오래된 스냅샷 삭제 (30일 이전)
aws ec2 describe-snapshots --owner-ids self \
--query "Snapshots[?StartTime<='$(date -d '30 days ago' +%Y-%m-%d)'].SnapshotId" \
--output text | xargs -I {} aws ec2 delete-snapshot --snapshot-id {}

실습 — 백업 스크립트 실행과 복구 테스트
아래 백업 스크립트를 /opt/scripts/backup.sh로 저장하고 실행합니다. 실무에서 쓰는 백업 스크립트의 기본 구조입니다.
#!/bin/bash
# /opt/scripts/backup.sh
BACKUP_DIR="/backup/$(date +%Y%m%d)"
LOG_FILE="/var/log/backup.log"
mkdir -p "$BACKUP_DIR"
echo "[$(date)] 백업 시작" >> "$LOG_FILE"
# 1. 설정 파일 백업
tar -czf "$BACKUP_DIR/config.tar.gz" \
/etc/nginx/ \
/opt/tomcat/conf/ 2>> "$LOG_FILE"
echo "[$(date)] 설정 파일 백업 완료" >> "$LOG_FILE"
# 2. MySQL 백업 (비밀번호는 /root/.my.cnf에 저장)
mysqldump --single-transaction --all-databases 2>> "$LOG_FILE" | \
gzip > "$BACKUP_DIR/mysql.sql.gz"
echo "[$(date)] MySQL 백업 완료" >> "$LOG_FILE"
# 3. 원격 rsync (backup-server가 접근 가능한 경우)
# rsync -avz "$BACKUP_DIR/" backup-server:/backup/$(hostname)/ 2>> "$LOG_FILE"
echo "[$(date)] 백업 완료: $BACKUP_DIR" >> "$LOG_FILE"
# 7일 이전 백업 삭제
find /backup/ -maxdepth 1 -type d -mtime +7 -exec rm -rf {} \; 2>> "$LOG_FILE"
sudo bash /opt/scripts/backup.sh/backup/YYYYMMDD/디렉터리가 생성됐는지 확인 (ls -la /backup/)config.tar.gz와mysql.sql.gz파일이 0바이트 이상인지 확인 (ls -lh /backup/YYYYMMDD/)/var/log/backup.log에백업 완료메시지가 기록됐는지 확인
백업이 있다고 안심하면 안 됩니다. 복구가 실제로 되는지 정기적으로 테스트해야 합니다. 개발 환경의 별도 DB(test_restore)에 복구해 봅니다.
# 테스트용 DB 생성
mysql -u root -p -e "CREATE DATABASE test_restore;"
# 백업에서 복구
gunzip -c /backup/$(date +%Y%m%d)/mysql.sql.gz | mysql -u root -p test_restore
# 복구 결과 확인
mysql -u root -p test_restore -e "SHOW TABLES;"
mysql -u root -p test_restore -e "SELECT COUNT(*) FROM users;" # 레코드 수 확인
# 설정 파일 복구 테스트
tar -tzf /backup/$(date +%Y%m%d)/config.tar.gz # 내용 확인
tar -xzf /backup/$(date +%Y%m%d)/config.tar.gz -C /tmp/restore_test/ # 임시 경로에 풀기
ls /tmp/restore_test/etc/nginx/ # 파일 존재 확인
# 테스트용 DB 삭제
mysql -u root -p -e "DROP DATABASE test_restore;"
gunzip -c /backup/$(date +%Y%m%d)/mysql.sql.gz | mysql -u root -p test_restore- 백업 스크립트 실행 후 /backup/YYYYMMDD/ 디렉터리가 생성됐는가
- config.tar.gz와 mysql.sql.gz 파일이 0바이트 이상으로 생성됐는가
- /var/log/backup.log에 '백업 완료' 메시지가 기록됐는가
- 복구 테스트에서 test_restore DB에 테이블이 정상 생성됐는가
- 레코드 수가 원본과 일치하는가 (COUNT(*) 비교)
DR(재해복구) 개념
Active-Passive DR과 Active-Active DR
백업은 "데이터 보호"이고, DR(Disaster Recovery)은 "서비스 연속성"입니다. 서버 건물이 화재로 소실됐을 때 다른 곳에서 서비스를 재개하는 것이 DR의 목표입니다.
DR 구성 유형:
| 유형 | 구성 | RTO | RPO | 비용 | 특징 |
|---|---|---|---|---|---|
| Active-Passive | 주 센터 운영 중, 백업 센터 대기 | 수 시간 | 수 시간~1일 | 중간 | 가장 일반적 |
| Active-Active | 두 센터 모두 운영, 트래픽 분산 | 수 분 | 거의 0 | 높음 | 대형 서비스 |
| Cold Standby | 백업 센터 꺼진 상태 | 수일 | 마지막 백업 | 낮음 | 소규모/비핵심 |
Active-Passive DR 흐름:
평상시: 주 센터 운영, 백업 센터에 rsync/복제로 데이터 동기화
장애시: DNS를 백업 센터 IP로 변경
→ 서비스 재개 (RTO: DNS TTL + 기동 시간)
Active-Active DR 흐름:
평상시: 두 센터 모두 트래픽 처리 (로드밸런서로 분산)
장애시: 한 센터 장애 → 자동으로 다른 센터로 100% 전환
→ 서비스 중단 거의 없음 (RTO: 수 초~수 분)
복구 테스트가 중요한 이유:
실제 사례:
백업은 3년간 매일 실행
→ 어느 날 복구 시도
→ mysqldump 파일이 깨져 있음 (디스크 용량 부족으로 중간에 잘린 파일)
→ 실제 복구 불가
예방:
월 1회 개발환경에서 복구 테스트 실행
복구 성공 여부를 로그로 기록
정기 리포트로 백업 상태 모니터링
트러블슈팅
원인: 백업 중 디스크 용량 부족, 네트워크 단절, 또는 mysqldump 실행 중 DB 연결 끊김으로 파일이 중간에 잘립니다. 파일 크기는 있어도 내용이 불완전한 경우입니다.
# 백업 파일 무결성 확인
gunzip -t /backup/mysql_20260530.sql.gz
# 출력이 없으면 파일 정상, gzip: ... unexpected end of file 이면 파일 손상
# SQL 파일 끝부분 확인 (정상적인 mysqldump 결과의 마지막 줄)
gunzip -c /backup/mysql_20260530.sql.gz | tail -5
# 정상: -- Dump completed on 2026-05-30 02:15:31
# 백업 스크립트에 검증 추가
mysqldump --single-transaction --all-databases | gzip > /backup/mysql.sql.gz
if [ ${PIPESTATUS[0]} -ne 0 ]; then
echo "백업 실패!" | mail -s "BACKUP FAILED" admin@example.com
fi
# 디스크 용량 사전 확인 (백업 전)
df -h /backup
# Use% 80% 이상이면 백업 전 정리 필요
예방: 백업 스크립트에 종료 코드 체크와 파일 크기 최소 임계값 검증을 추가합니다. 월 1회 복구 테스트를 캘린더에 등록해 습관화합니다.
원인: 로그 테이블(audit_log, access_log 등)이 수백만 건 쌓인 상태에서 전체 dump를 받으면 수십 GB가 되는 경우가 있습니다. 이 테이블들은 백업이 필요 없는 경우가 많습니다.
# 테이블별 크기 확인 (큰 테이블 찾기)
mysql -u root -p -e "
SELECT table_schema, table_name,
ROUND(data_length/1024/1024, 2) AS data_MB,
ROUND(index_length/1024/1024, 2) AS index_MB
FROM information_schema.tables
ORDER BY data_length DESC
LIMIT 20;"
# 특정 테이블 제외하고 백업
mysqldump --single-transaction \
--ignore-table=mydb.audit_log \
--ignore-table=mydb.access_log \
mydb | gzip > /backup/mydb_no_logs_$(date +%Y%m%d).sql.gz
# 데이터 없이 스키마만 백업 (로그 테이블은 구조만)
mysqldump --no-data mydb audit_log > /backup/audit_log_schema.sql
# 로그 테이블 주기적 정리 (30일 이전 데이터 삭제)
mysql -u root -p mydb -e "
DELETE FROM audit_log WHERE created_at < DATE_SUB(NOW(), INTERVAL 30 DAY);"
예방: 백업 전략 수립 시 테이블별 백업 필요성을 분류합니다. 로그·임시·캐시 테이블은 제외 목록에 넣고 문서화합니다.
실제 업무에서 이 흐름이 생명줄이 되는 상황:
백업은 평소에는 존재 자체를 잊게 만드는 것이 잘 된 백업입니다. 그러나 사고가 났을 때 없으면 모든 것이 끝납니다. 실무에서 주니어가 바로 적용할 수 있는 최소 기준:
# 1. 지금 당장 실행: 우리 서버에 백업이 돌고 있는가 확인
crontab -l | grep backup
ls -lt /backup/ | head -10 # 가장 최근 백업 날짜 확인
# 2. 백업이 없다면: 최소 스크립트라도 crontab에 등록
# 3. 백업이 있다면: 복구 테스트 한 번이라도 해보기
# 복구 테스트 기록 템플릿
echo "$(date): 복구 테스트 성공 — DB 레코드 수 일치, 설정 파일 정상" >> /var/log/restore_test.log
RTO/RPO를 서비스 오너(개발 리더, 기획팀)와 합의해두는 것도 인프라팀의 역할입니다. "4시간 내 복구"를 목표로 한다면 백업 주기, 복구 절차, DR 구성이 모두 그에 맞게 설계되어야 합니다. 다음 모듈에서는 온프레미스와 클라우드 인프라의 차이, AWS EC2/RDS/S3/CloudWatch 운영 실무를 다룹니다.