백업 / 복구(WAL)
WAL(Write-Ahead Logging)은 데이터 파일의 변경을 디스크에 쓰기 전에 그 변경 내용을 로그에 먼저 기록하는 방식입니다. 기본 설정에서 커밋은 로그가 디스크에 닿으면 완료되고, 장애가 나도 로그를 재생해 커밋된 변경을 복구할 수 있습니다.
백업과 복구는 이 로그를 한 단계 더 활용합니다. 정기적으로 받은 베이스 백업에 보관해 둔 WAL을 이어서 재생하면 원하는 시점의 상태로 되돌릴 수 있고, 이를 PITR(Point-In-Time Recovery)이라 합니다. 이 토픽에서는 WAL의 쓰기 순서, 장애 뒤 재생 과정, PITR을 시각화로 확인합니다.
핵심 개념
WAL 원칙
데이터 페이지를 디스크에 쓰기 전에 그 변경을 설명하는 로그 레코드를 먼저 디스크에 기록합니다.
핵심 개념
체크포인트
메모리의 더티 페이지를 데이터 파일에 기록하는 시점입니다. 복구는 마지막 체크포인트가 가리키는 지점의 로그부터 재생하면 됩니다.
핵심 개념
redo (재생)
장애 뒤 재시작할 때 로그를 순서대로 다시 적용해 데이터 파일에 반영되지 못한 커밋된 변경을 복원하는 과정입니다.
핵심 개념
베이스 백업
데이터베이스 파일 전체를 복사해 둔 백업입니다. 복사 중 생긴 불일치는 WAL 재생이 바로잡아 주므로 PITR의 출발점이 됩니다.
핵심 개념
WAL 아카이브
서버가 채운 WAL 세그먼트를 안전한 곳에 계속 보관하는 것입니다. 베이스 백업 이후의 변경을 이어서 재생하려면 필요합니다.
핵심 개념
PITR
베이스 백업에 WAL을 재생하다 원하는 시점에서 멈춰 그 시점의 상태로 복구하는 방법입니다. 잘못된 DELETE 직전으로 돌아가는 것이 대표적인 용도입니다.
WAL과 백업의 배경
변경을 로그에 먼저 기록해 장애에서 복구한다는 발상은 데이터베이스 복구 이론의 기본입니다. 모한(C. Mohan) 등이 1992년에 발표한 ARIES 알고리즘이 WAL을 바탕으로 한 복구 방식을 체계적으로 정리했고, IBM DB2 등 여러 시스템에 구현되었습니다. ARIES는 로그 재생(redo)과 미완료 트랜잭션 되돌리기(undo)를 결합하고, PostgreSQL은 undo 단계 없이 MVCC 규칙으로 커밋되지 않은 변경을 보이지 않게 둡니다.
PostgreSQL은 WAL을 크래시 복구뿐 아니라 복제와 PITR에도 사용합니다. 백업에는 SQL 문으로 데이터를 내보내는 논리 백업(pg_dump)과 파일을 복사하는 물리 백업(pg_basebackup)이 있고, PITR은 물리 백업과 WAL 아카이브를 이용합니다.
숫자로 확인하기
자주 하는 오해
면접에서 이렇게 물어요
면접 질문
WAL의 핵심 원칙은?
데이터 파일을 변경하기 전에 변경 내용을 먼저 로그에 기록한다는 것입니다. 로그는 해당 데이터 페이지보다 먼저 디스크에 있어야 합니다.
면접 질문
WAL 덕분에 가능한 것은?
장애 뒤 로그를 재생(redo)해 커밋된 변경을 복구할 수 있습니다. 데이터 파일에 반영되지 못한 커밋 내용도 로그에는 남아 있기 때문입니다.
면접 질문
PITR이란?
베이스 백업과 WAL 아카이브로 원하는 특정 시점의 상태로 복구하는 방식입니다. 잘못된 DELETE 직전으로 되돌리는 것이 대표적인 사용 예입니다.
면접 질문
논리 백업과 물리 백업은 어떻게 다른가요?
논리 백업은 SQL 등으로 데이터를 내보내 이식성이 좋지만 복원 때 SQL을 다시 실행하고 인덱스를 다시 만들어야 하고 PITR에는 쓸 수 없습니다. 물리 백업은 파일을 복사하므로 PITR이 가능하지만 같은 메이저 버전과 머신 아키텍처에서 클러스터 전체 단위로만 복원합니다.
활용 분야
함께 보면 좋은 토픽
참고 자료
SQL — PostgreSQL의 WAL 설정과 PITR 준비
SHOW wal_level; -- WAL에 기록하는 정보의 양 (기본 replica) SHOW synchronous_commit; -- on: 커밋 때 WAL을 디스크에 기록하고 응답 SELECT pg_current_wal_lsn(); -- 현재 WAL 쓰기 위치 CHECKPOINT; -- 체크포인트를 바로 실행 (보통은 자동) -- postgresql.conf: WAL 아카이브 켜기 -- archive_mode = on -- archive_command = 'test ! -f /archive/%f && cp %p /archive/%f' -- 셸: 베이스 백업 -- pg_basebackup -D /backup/base -Ft -z -P -- 복구할 때 (postgresql.conf) -- restore_command = 'cp /archive/%f %p' -- recovery_target_time = '2026-10-06 14:30:00' -- 데이터 디렉터리에 recovery.signal 파일을 만들고 서버를 시작
PostgreSQL은 모든 변경을 WAL에 먼저 기록하며, synchronous_commit이 on이면 커밋 때 WAL을 디스크에 기록한 뒤 응답합니다. CHECKPOINT는 더티 페이지를 데이터 파일에 쓰는 체크포인트를 즉시 실행하며 superuser 또는 pg_checkpoint 롤만 실행할 수 있고, 평소에는 서버가 자동으로 실행합니다. 동기 대기 서버를 설정하면 on은 그 서버의 기록까지 기다립니다. 시각화의 WAL 원칙 탭이 보여 주는 순서입니다. PITR을 쓰려면 archive_mode와 archive_command로 WAL을 보관하고, pg_basebackup으로 베이스 백업을 받아 둡니다. 복구할 때는 베이스 백업을 복원한 뒤 restore_command로 보관한 WAL을 가져오고 recovery_target_time으로 멈출 시점을 정해, recovery.signal 파일을 만들고 서버를 시작합니다(PostgreSQL 12 이상). 경로와 시각은 환경마다 다르고, 자세한 절차는 PostgreSQL 문서를 따라야 합니다.
Python — WAL과 장애 복구 모델
class Db: def __init__(self): self.disk = {'x': 1, 'y': 5} # 데이터 파일 self.wal = [] # 디스크의 WAL self.mem = dict(self.disk) # 메모리 버퍼 self.ckpt = 0 # 마지막 체크포인트 이후 WAL 시작 위치 def write(self, tx, key, value): self.wal.append((tx, 'SET', key, self.mem[key], value)) # 로그를 먼저 기록 self.mem[key] = value # 그 다음 메모리 페이지 변경 def commit(self, tx): self.wal.append((tx, 'COMMIT')) # 커밋 기록이 디스크에 닿으면 커밋 완료 def checkpoint(self): self.disk = dict(self.mem) # 더티 페이지를 데이터 파일에 기록 self.ckpt = len(self.wal) def crash_and_recover(self): self.mem = dict(self.disk) # 메모리는 사라지고 데이터 파일만 남음 committed = {r[0] for r in self.wal[self.ckpt:] if r[1] == 'COMMIT'} for r in self.wal[self.ckpt:]: # 체크포인트 이후 WAL 재생(redo) if r[1] == 'SET' and r[0] in committed: self.mem[r[2]] = r[4] return self.mem db = Db() db.write('T1', 'x', 2) db.commit('T1') # T1 커밋 (데이터 파일에는 아직 x = 1) db.write('T2', 'y', 9) # T2는 커밋하지 못한 채 장애 print('장애 직전:', db.mem, '데이터 파일:', db.disk) print('복구 결과:', db.crash_and_recover()) print('WAL:', db.wal) # 장애 직전: {'x': 2, 'y': 9} 데이터 파일: {'x': 1, 'y': 5} # 복구 결과: {'x': 2, 'y': 5} # WAL: [('T1', 'SET', 'x', 1, 2), ('T1', 'COMMIT'), ('T2', 'SET', 'y', 5, 9)]
WAL의 순서를 옮긴 장난감 모델입니다. write는 로그를 먼저 쓰고 메모리를 바꾸며, commit은 커밋 기록을 남깁니다. T1이 커밋하고 T2가 커밋하지 못한 채 장애가 나면 메모리의 x = 2, y = 9는 사라지고 데이터 파일에는 옛 값 x = 1, y = 5만 남습니다. 복구는 체크포인트 이후의 WAL을 재생하되 이 모델은 설명을 위해 커밋 기록이 있는 T1만 반영하므로 x = 2, y = 5가 됩니다. 실제 PostgreSQL은 로그를 모두 재생한 뒤 커밋되지 않은 변경을 MVCC 규칙으로 보이지 않게 둡니다. 시각화의 장애 복구 탭과 같은 결과입니다. 실제 데이터베이스는 페이지 단위의 로그, 일련번호(LSN), 동시에 실행되는 여러 트랜잭션을 함께 다루므로 훨씬 복잡합니다.
Java — 베이스 백업에 WAL을 재생하는 PITR 모델
record Wal(int t, String op, String key, Integer val) {} static Map<String, Integer> restore(Map<String, Integer> base, List<Wal> archive, int target) { Map<String, Integer> db = new TreeMap<>(base); // 베이스 백업을 복원 for (Wal w : archive) { if (w.t() > target) break; // 복구 목표 시점에서 멈춤 if (w.op().equals("SET")) db.put(w.key(), w.val()); else db.remove(w.key()); } return db; } Map<String, Integer> base = Map.of("x", 1, "y", 5); // t = 0의 베이스 백업 List<Wal> archive = List.of( new Wal(1, "SET", "x", 2), new Wal(3, "SET", "y", 9), new Wal(4, "SET", "x", 3), new Wal(5, "DELETE", "x", null)); System.out.println("t=4까지: " + restore(base, archive, 4)); // t=4까지: {x=3, y=9} System.out.println("끝까지: " + restore(base, archive, 8)); // 끝까지: {y=9}
PITR의 핵심을 옮긴 모델입니다. 베이스 백업(x = 1, y = 5)을 복원한 뒤 WAL을 시각 순서대로 재생하되 목표 시점을 넘으면 멈춥니다. 목표를 t = 4로 하면 x = 3, y = 9가 되어 t = 5의 DELETE가 반영되지 않은 사고 직전 상태가 되고, 끝까지 재생하면 x가 지워진 {y=9}가 됩니다. 시각화의 PITR 탭이 보여 주는 결과입니다. 실제 PITR은 WAL 레코드를 파일 단위로 아카이브에서 가져와 순서대로 재생하며, 시각 외에 트랜잭션 번호나 위치 같은 값으로도 멈출 지점을 정할 수 있습니다. 이 코드는 개념 설명용입니다.
Q1
WAL(Write-Ahead Logging)의 핵심 원칙은?
Q2
WAL 덕분에 가능해지는 것은?
Q3
PITR(Point-In-Time Recovery)이란?