Skip to main content
Summary

백업의 목적은 파일 보관이 아니라 복구 가능성이다

데이터베이스 백업은 "언젠가 필요할지도 모르는 파일"을 만들어 두는 일이 아니다. 장애가 났을 때, 실수로 데이터를 지웠을 때, 배포 후 문제가 생겼을 때 원하는 상태로 되돌아갈 수 있어야 백업이다.

PostgreSQL 백업은 크게 세 가지 관점으로 나눌 수 있다. SQL 형태로 데이터를 뽑는 논리 백업, 데이터 파일 자체를 보관하는 파일 시스템 또는 물리 백업, 그리고 WAL을 함께 보관해 특정 시점까지 되돌리는 연속 아카이빙 방식이다. PostgreSQL 공식 문서도 백업에는 서로 다른 접근법이 있으며 각 방식마다 장단점이 있다고 설명한다.1

실무에서는 한 가지 방식만 고집하지 않는다.

  • 특정 테이블이나 데이터베이스를 옮기려면 pg_dump가 편하다.

  • 서버 전체 장애에 대비하려면 물리 백업이 필요하다.

  • "어제 새벽 2시 13분 직전"으로 돌아가야 한다면 WAL 아카이빙과 PITR이 필요하다.

핵심은 간단하다. 백업 정책은 반드시 복구 시나리오와 함께 설계해야 한다. "무엇을 백업할 것인가"보다 먼저 "어떤 사고가 났을 때 어디까지 되돌릴 것인가"를 정해야 한다.

백업, 복구, 마이그레이션AI 생성 이미지

논리 백업: pg_dump로 데이터베이스를 파일로 남기기

논리 백업은 테이블 구조와 데이터를 SQL 또는 PostgreSQL 전용 아카이브 형식으로 추출하는 방식이다. 파일 크기가 비교적 작고 다른 서버나 새 버전으로 옮기기 좋다. 대신 큰 데이터베이스에서는 백업과 복구 시간이 길어질 수 있다. 논리 백업은 이식성과 선택 복구에 강하고, 물리 백업은 전체 클러스터 복구와 재해 복구에 강하다는 차이가 있다.2

먼저 shop 데이터베이스를 일반 SQL 파일로 백업해 보자.

pg_dump -h localhost -U app -d shop -f shop_backup.sql

이 파일은 텍스트이므로 열어 볼 수 있다. 내부에는 CREATE TABLE, COPY, ALTER TABLE 같은 SQL이 들어 있다. 복구는 psql로 실행한다.

createdb -h localhost -U app shop_restore
psql -h localhost -U app -d shop_restore -f shop_backup.sql

복구 후에는 행 수를 비교한다.

SELECT count(*) FROM customers;
SELECT count(*) FROM orders;
SELECT count(*) FROM order_items;

운영에서는 일반 SQL 파일보다 커스텀 형식이 더 자주 쓰인다.

pg_dump -h localhost -U app -d shop -F c -f shop_backup.dump

-F c는 custom format이다. 이 형식은 pg_restore로 복구하며, 특정 테이블만 복구하거나 복구 순서를 제어하기 쉽다.

createdb -h localhost -U app shop_restore
pg_restore -h localhost -U app -d shop_restore shop_backup.dump

복구 전에 어떤 객체가 들어 있는지 목록을 볼 수도 있다.

pg_restore -l shop_backup.dump

특정 테이블만 복구해야 한다면 다음처럼 실행한다.

pg_restore -h localhost -U app \
  -d shop_restore \
  -t order_items \
  shop_backup.dump

다만 -t로 테이블 하나만 지정하면 그 테이블과 직접 관련된 모든 의존 객체가 항상 자동으로 함께 복구된다고 기대해서는 안 된다. 이미 스키마가 준비된 데이터베이스에 일부 데이터만 되살리는 용도인지, 빈 데이터베이스에 테이블 구조까지 새로 만드는 용도인지에 따라 복구 목록을 미리 확인하는 습관이 필요하다.

대용량 데이터베이스에서는 디렉터리 형식과 병렬 작업을 검토한다. 디렉터리 형식은 여러 파일로 백업되며 병렬 백업과 병렬 복구에 적합하다.3

pg_dump -h localhost -U app -d shop -F d -j 4 -f shop_dir_backup

createdb -h localhost -U app shop_restore
pg_restore -h localhost -U app -d shop_restore -j 4 shop_dir_backup

여기서 -j 4는 병렬 작업 수다. 숫자를 크게 한다고 항상 빨라지지는 않는다. CPU, 디스크 I/O, 네트워크, PostgreSQL 연결 수가 함께 버텨야 한다. 운영 서버에서 백업을 실행하기 전에는 낮은 부하 시간대와 별도 복제 서버 사용을 검토한다.

pg_dumpall: 클러스터 공통 정보까지 백업하기

pg_dump는 데이터베이스 하나를 백업한다. 하지만 PostgreSQL 클러스터에는 여러 데이터베이스가 있고, role, tablespace 같은 전역 객체도 있다. 이런 정보는 데이터베이스 개별 덤프만으로는 충분히 남지 않는다.

전체 클러스터를 SQL 파일로 백업하려면 다음과 같이 실행한다.

pg_dumpall -h localhost -U postgres -f all_databases.sql

복구는 새 클러스터에 접속해 실행한다.

psql -h localhost -U postgres -f all_databases.sql

전역 객체만 따로 남길 수도 있다.

pg_dumpall -h localhost -U postgres --globals-only -f globals.sql

이 파일에는 role 생성, 권한, tablespace 같은 정보가 들어간다. 사용자, 권한, 보안 설계에서 권한을 role 단위로 나누어 설계했다면, 데이터베이스 백업과 함께 전역 role 백업도 보관해야 한다. 데이터만 복구했는데 애플리케이션 계정이 없거나 권한이 다르면 서비스는 정상 동작하지 않는다.

다만 운영 환경에서는 모든 것을 한 번에 pg_dumpall로 해결하려 하기보다 다음처럼 나누는 편이 관리하기 좋다.

pg_dumpall --globals-only -f globals.sql
pg_dump -d shop -F c -f shop.dump
pg_dump -d analytics -F c -f analytics.dump

이렇게 하면 데이터베이스별로 복구 우선순위를 정할 수 있다. 예를 들어 주문 시스템은 먼저 복구하고, 분석용 데이터베이스는 나중에 복구하는 식으로 대응할 수 있다.

복구 검증: 백업 파일이 아니라 결과를 확인한다

백업 스크립트가 성공했다는 메시지만으로는 부족하다. 복구 가능한 백업인지 정기적으로 검증해야 한다.

검증 절차는 다음처럼 단순하게 시작할 수 있다.

  1. 임시 데이터베이스를 만든다.

  2. 백업 파일을 복구한다.

  3. 핵심 테이블의 행 수를 비교한다.

  4. 제약 조건과 시퀀스 값을 확인한다.

  5. 애플리케이션의 주요 조회 쿼리를 실행한다.

예를 들어 시퀀스 값은 복구 후 자주 문제가 된다. orders.id가 bigserial 또는 identity 열이라면 다음에 발급될 id가 현재 최대 id와 충돌해서는 안 된다.

SELECT max(id) FROM orders;

SELECT last_value
FROM orders_id_seq;

필요하면 다음처럼 맞춘다.

SELECT setval('orders_id_seq', (SELECT max(id) FROM orders));

이렇게 하면 다음 nextval은 현재 최대 id보다 큰 값을 돌려준다. 단, 빈 테이블에서는 max(id)가 NULL이므로 주의해야 한다. 빈 테이블까지 고려하면 다음처럼 쓸 수 있다.

SELECT setval(
  'orders_id_seq',
  coalesce((SELECT max(id) FROM orders), 1),
  (SELECT max(id) IS NOT NULL FROM orders)
);

세 번째 인자는 is_called 값이다. true이면 다음 nextval이 한 번 증가한 값을 반환하고, false이면 방금 지정한 값을 그대로 반환한다. 그래서 테이블에 행이 있으면 최대 id 다음 값을 쓰게 하고, 테이블이 비어 있으면 첫 값부터 자연스럽게 쓰게 만들 수 있다.

복구 검증에서 중요한 것은 "서비스가 다시 시작될 수 있는가"다. 단순히 테이블이 만들어졌는지보다 로그인, 주문 조회, 정산 집계, 관리자 검색처럼 실제 업무 흐름을 확인해야 한다.

파일 시스템 백업과 WAL: 전체 서버를 되살리는 방식

논리 백업은 이식성이 좋지만, 매우 큰 데이터베이스 전체를 빠르게 복구해야 할 때는 한계가 있다. 이때는 물리 백업을 사용한다. 물리 백업은 PostgreSQL 데이터 디렉터리의 파일을 기준으로 클러스터 전체를 복구하는 방식이다.

하지만 실행 중인 데이터베이스 파일을 단순히 복사하면 안전하지 않다. 복사 중에도 데이터 파일은 계속 바뀐다. 일관된 물리 백업을 만들려면 PostgreSQL이 인식하는 base backup 절차를 사용해야 한다. 실무에서는 pg_basebackup 또는 pgBackRest, WAL-G 같은 전용 도구를 많이 쓴다.

가장 단순한 base backup 예시는 다음과 같다.

pg_basebackup -h localhost -U repl_user \
  -D /backup/base_20260530 \
  -Fp -Xs -P

옵션의 의미는 다음과 같다.

  • -D: 백업을 저장할 디렉터리

  • -Fp: 일반 파일 형식으로 저장

  • -Xs: 필요한 WAL을 스트리밍으로 함께 받음

  • -P: 진행 상황 표시

pg_basebackup을 실행하는 계정에는 보통 복제 권한이 필요하다. 운영 환경에서는 백업 전용 계정을 만들고, 필요한 권한만 부여하는 편이 안전하다.

여기서 WAL은 Write-Ahead Log의 약자다. PostgreSQL은 데이터 파일을 바꾸기 전에 변경 기록을 WAL에 먼저 남긴다. 그래서 base backup 이후의 WAL을 모두 보관하면, 백업 시점 이후의 변경을 다시 적용할 수 있다.

WAL 아카이빙을 사용하려면 postgresql.conf에 다음과 같은 설정이 필요하다.

archive_mode = on
archive_command = 'test ! -f /archive/%f && cp %p /archive/%f'

archive_command는 WAL 파일 하나를 안전한 저장소로 복사하는 명령이다. 운영에서는 로컬 디스크 하나에만 저장하지 않는다. 서버 장애에 대비하려면 별도 디스크, 다른 서버, 오브젝트 스토리지 같은 외부 저장소가 필요하다. 또한 WAL 아카이브가 끊기면 특정 시점 복구도 끊긴다. 단순히 base backup만 있는지 보지 말고, 그 이후의 WAL이 빠짐없이 보관되고 있는지도 함께 확인해야 한다.

PITR: 실수 직전으로 되돌리기

PITR(Point-In-Time Recovery)은 특정 시점 복구를 뜻한다. 예를 들어 운영자가 실수로 다음 SQL을 실행했다고 하자.

DELETE FROM orders;

일반 복제 서버가 있어도 안심할 수 없다. 실수한 DELETE도 복제될 수 있기 때문이다. 이때 base backup과 WAL 아카이브가 있으면 "삭제 직전 시각"까지만 WAL을 재생해 복구할 수 있다. WAL 아카이빙과 PITR은 실수로 테이블을 삭제하거나 잘못된 배포가 반영된 상황에서 안전한 시점으로 돌아가는 데 사용된다.4

복구 흐름은 개념적으로 다음과 같다.

  1. PostgreSQL 서버를 중지한다.

  2. 손상된 데이터 디렉터리를 보존하거나 비운다.

  3. base backup을 데이터 디렉터리에 복원한다.

  4. WAL 아카이브 위치를 restore_command로 지정한다.

  5. recovery_target_time을 실수 직전으로 지정한다.

  6. recovery.signal 파일을 만들고 서버를 시작한다.

  7. 복구 결과를 확인한 뒤 서비스 전환 여부를 결정한다.

설정 예시는 다음과 같다.

restore_command = 'cp /archive/%f %p'
recovery_target_time = '2026-05-30 02:12:50+09'
recovery_target_action = 'pause'

그리고 데이터 디렉터리에 recovery.signal 파일을 둔다.

touch $PGDATA/recovery.signal

recovery_target_action = 'pause'로 두면 목표 시점에 도달했을 때 PostgreSQL이 WAL 재생을 멈추고 대기한다. 이때 데이터를 확인한다. 원하는 시점이 맞다면 다음 명령으로 재생을 재개해 복구를 마무리한다.

SELECT pg_wal_replay_resume();

이 명령은 단순히 "조금 더 재생한다"는 뜻이 아니라, 목표 지점에서 멈춘 복구 상태를 끝내고 서버가 새 타임라인에서 열리도록 하는 절차로 이해하면 된다. 만약 확인해 보니 목표 시점이 너무 이르거나 늦었다면 그대로 진행하지 말고 서버를 중지한 뒤 복구 목표 시각을 다시 정해 재시도해야 한다.

PITR은 강력하지만 연습 없이 운영 사고 때 처음 사용하면 위험하다. WAL이 빠져 있거나, base backup이 너무 오래되었거나, 목표 시각을 잘못 잡으면 원하는 상태로 돌아가지 못한다. 그래서 PITR은 반드시 별도 환경에서 정기적으로 리허설해야 한다.

변경 작업 전 백업과 롤백 계획

스키마 변경, 대량 데이터 적재, 운영 데이터 수정은 모두 작은 장애를 큰 장애로 만들 수 있는 작업이다. 변경 전에는 다음 순서로 진행한다.

첫째, 변경 대상을 조회한다.

SELECT count(*)
FROM products
WHERE discontinued_at IS NULL
  AND stock_quantity = 0;

둘째, 변경 전 백업을 남긴다. 전체 데이터베이스가 부담스럽다면 관련 테이블만 백업한다.

pg_dump -h localhost -U app -d shop \
  -F c \
  -t products \
  -f products_before_change.dump

셋째, 트랜잭션 안에서 변경하고 결과를 확인한다.

BEGIN;

UPDATE products
SET discontinued_at = now()
WHERE discontinued_at IS NULL
  AND stock_quantity = 0
RETURNING id, name, discontinued_at;

-- 예상 결과와 다르면
-- ROLLBACK;

COMMIT;

넷째, 운영 반영 후 검증 쿼리를 실행한다.

SELECT count(*)
FROM products
WHERE discontinued_at IS NOT NULL
  AND stock_quantity = 0;

다섯째, 롤백 방법을 문서화한다. 롤백은 "문제가 생기면 되돌린다"가 아니다. 어떤 명령으로, 어느 백업에서, 어느 시점까지 되돌릴지 정해 두어야 한다.

대량 적재 전에는 임시 테이블을 활용하면 안전하다.

CREATE TEMP TABLE import_products (
  sku text,
  name text,
  price numeric(12,2)
);

CSV를 먼저 임시 테이블에 넣고 검증한다.

SELECT count(*) FROM import_products;

SELECT sku, count(*)
FROM import_products
GROUP BY sku
HAVING count(*) > 1;

검증이 끝난 뒤 본 테이블에 반영한다.

INSERT INTO products (sku, name, price)
SELECT sku, name, price
FROM import_products
ON CONFLICT (sku) DO UPDATE
SET name = EXCLUDED.name,
    price = EXCLUDED.price;

이 흐름은 백업, 검증, 반영, 확인, 롤백 계획을 하나의 작업 단위로 묶는다.

마이그레이션 점검: 옮긴 뒤 같아야 한다

마이그레이션은 백업과 복구의 확장판이다. 다른 DBMS에서 PostgreSQL로 옮기거나, 오래된 PostgreSQL에서 새 버전으로 올릴 때는 단순히 데이터가 들어왔는지만 보면 안 된다.

먼저 데이터 타입을 확인한다.

  • 금액은 double precision보다 numeric이 적합한가?

  • 날짜와 시각은 timestamp인지 timestamptz인지 정했는가?

  • 문자열 길이 제한이 업무 규칙인지 과거 시스템의 흔적인가?

  • boolean으로 줄인 상태값이 나중에 세 개 이상으로 늘어나지 않는가?

다음으로 인코딩을 확인한다.

SHOW server_encoding;
SHOW client_encoding;

한국어 데이터는 보통 UTF8을 기준으로 맞춘다. 마이그레이션 후 깨진 문자가 발견되면 원본 인코딩, 덤프 파일 인코딩, 클라이언트 인코딩을 함께 확인해야 한다.

확장 기능도 점검한다.

SELECT extname, extversion
FROM pg_extension
ORDER BY extname;

원본에서 pgcrypto, uuid-ossp, PostGIS 같은 확장을 사용했다면 대상 서버에도 설치되어 있어야 한다.

마지막으로 시퀀스와 제약 조건을 확인한다.

SELECT conname, contype, conrelid::regclass AS table_name
FROM pg_constraint
WHERE convalidated = false;

행 수 비교도 기본이다.

SELECT 'customers' AS table_name, count(*) FROM customers
UNION ALL
SELECT 'orders', count(*) FROM orders
UNION ALL
SELECT 'order_items', count(*) FROM order_items;

가능하면 원본과 대상에서 같은 쿼리를 실행해 결과를 비교한다. 특히 정산 금액, 주문 수, 회원 수, 최근 7일 매출처럼 업무적으로 중요한 숫자는 반드시 맞춰야 한다.

백업 정책을 세울 때 물어야 할 질문

좋은 백업 정책은 도구 이름보다 질문에서 시작한다.

  • 어느 정도의 데이터 손실을 허용할 수 있는가?

  • 몇 분 안에 복구해야 하는가?

  • 테이블 하나만 되살릴 일이 많은가, 서버 전체 복구가 중요한가?

  • 백업 파일은 운영 서버와 다른 위치에 있는가?

  • WAL 아카이브는 빠짐없이 쌓이고 있으며, 오래된 WAL을 언제까지 보관할 것인가?

  • 복구 테스트를 언제, 누가, 어떤 절차로 수행하는가?

  • 백업 계정과 마이그레이션 계정의 권한은 최소화되어 있는가?

학습 환경에서는 pg_dump와 pg_restore부터 익히면 충분하다. 운영 환경에서는 여기에 전역 객체 백업, 물리 백업, WAL 아카이빙, PITR 리허설을 더해야 한다.

백업은 만들어 두는 것으로 끝나지 않는다. 복구해 보고, 검증하고, 시간을 재고, 절차를 남겨야 한다. 그때부터 백업은 실제 장애 대응 수단이 된다.

참고

1PostgreSQL Documentation: Backup and Restore

2Crunchy Data: Introduction to Postgres Backups

3Microsoft Learn: Best practices for pg_dump and pg_restore

4EDB: A Complete Guide to PostgreSQL Backup & Recovery

책 목록

PostgreSQL 입문부터 실전 운영까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기