---
title: "Point-in-Time Recovery 절차: full backup과 binlog를 조합하는 방법"
description: "MySQL 전체 백업과 연속된 binlog를 조합해 사고 직전 트랜잭션 경계까지 복구하는 절차와 GTID·검증·전환 기준을 설명한다."
tags: [ MySQL, 백업복구, 트랜잭션, 운영 ]
image: "mysql-report-bg.png"
published: "2026-09-29"
updated: "2026-09-29"
author: "MySQL 기술 노트"
source_url: ""
---

백업이 정상 종료되었다는 사실과 사고 직전 상태로 돌아갈 수 있다는 사실은 다르다. 새벽의 전체 백업만 복원하면 이후 정상 거래까지 잃는다. 반대로 최신 binlog를 끝까지 적용하면 잘못된 `DELETE`도 다시 실행된다. Point-in-Time Recovery(PITR)의 핵심은 **일관된 기준 상태를 복원하고, 그 뒤의 변경 이력을 빠짐없이 연결하되, 승인된 트랜잭션 경계에서 멈추는 것**이다.

이 글은 Community MySQL 8.0 이상을 중심으로 설명한다. 실제 검증은 MySQL 8.0.46의 격리된 원본·복원 인스턴스 두 개에서 수행했다. 단일 실험 데이터베이스의 전체 논리 백업, 두 binlog 파일에 걸친 정상 거래, 전체 행 삭제 사고, 다른 인스턴스로의 복원과 재적용을 확인했다. 운영 서버 전체의 계정·권한 이관, 대용량 복구 시간, Aurora 복원은 이 실험에 포함하지 않았다.

## 1. 복구할 것은 시계의 숫자가 아니라 커밋된 상태다

전체 백업은 기준 상태를 제공하고 binlog는 그 상태 이후의 논리적 변경을 제공한다. InnoDB redo는 물리적인 페이지 변경과 crash recovery에 사용되며, undo는 롤백과 과거 버전 읽기를 지원한다. 둘을 binlog 아카이브의 대체물로 취급하면 안 된다. 물리 백업에서는 먼저 백업 도구의 prepare 절차로 일관된 기준 상태를 만든 뒤, 그 백업이 제공하는 binlog 좌표부터 추가 변경을 적용한다.

복구 범위는 세 가지로 정의한다.

- **기준점 B**: 백업에 이미 반영된 이력과 앞으로 적용할 이력을 나누는 파일·위치 및 GTID 정보다. 백업 파일을 복사하기 시작한 시각과 동일하지 않을 수 있다.
- **목표 경계 T**: 포함할 마지막 정상 트랜잭션의 커밋 이후이면서, 포함하지 않을 첫 트랜잭션의 시작 이전인 경계다.
- **이력 연결**: B에서 T까지 필요한 binlog 파일이 원래 순서대로 모두 존재해야 한다. 중간 파일 하나가 없으면 그 이후 파일만으로 일반적인 전진 복구를 완성할 수 없다.

여기서 PITR은 잘못된 SQL 하나를 역연산하는 기능이 아니다. 사고 직전으로 돌아가면 **그 이후에 발생한 정상 거래도 원칙적으로 포함되지 않는다**. 사고 이후 거래만 선별해 다시 반영하는 작업은 별도의 데이터 보정이며, 삭제된 행을 전제로 한 후속 UPDATE, 외부 결제, 메시지 발행까지 재검토해야 한다.

```mermaid
flowchart LR
    A["일관된 전체 백업과 기준 좌표 B"] --> B["격리된 복원 인스턴스"]
    C["B 이후 연속된 binlog"] --> D["사고 트랜잭션과 목표 경계 T 식별"]
    D --> E["B부터 T 직전까지 재실행 스트림 생성"]
    E --> B
    B --> F["전체 행·업무 불변식·GTID 확인"]
    F --> G["이전 writer 격리와 서비스 전환 승인"]
```

## 2. 백업 때부터 복구 계약을 남긴다

### 2.1 데이터와 좌표가 같은 시점을 설명해야 한다

`mysqldump --single-transaction`은 InnoDB의 일관된 읽기를 이용한다. `--source-data=2`를 함께 사용하면 그 스냅샷과 연결되는 binlog 좌표를 주석으로 남긴다. 이 조합에서도 좌표·스냅샷 정합성을 확보하는 초기 단계에는 짧은 전역 읽기 잠금이 필요할 수 있다. 따라서 “온라인 백업이므로 잠금 영향이 전혀 없다”는 설명은 정확하지 않다.

덤프 진행 중의 `ALTER TABLE`, `DROP TABLE`, `TRUNCATE TABLE` 같은 DDL을 통제하고, 비트랜잭션 테이블은 별도 일관성 정책을 적용해야 한다. 복원이 필요한 저장 프로그램과 이벤트, 트리거의 포함 여부도 확인한다. `--databases pitr_demo`는 그 데이터베이스의 전체 백업이지 서버 전체의 계정·권한·설정을 포함하는 백업은 아니다.

아래 셸 명령은 실험에서 사용한 옵션을 운영자가 준비한 login path와 파일 경로로 표현한 것이다. `pitr-source`와 `pitr-restore`는 각각 원본과 **별도의 격리된 복원 서버**를 가리키도록 사전에 등록해야 한다. 운영 서버에서 실험용 생성·삭제 SQL을 실행하지 않는다.

```bash
mysqldump --login-path=pitr-source \
  --single-transaction --quick --source-data=2 \
  --set-gtid-purged=ON --routines --events --triggers \
  --no-tablespaces --databases pitr_demo > full.sql
```

실험에서는 `--source-data=2`를 지정했어도 MySQL 8.0.46 덤프에 다음과 같은 구형 명칭의 주석이 기록되었다. 명칭보다 **해당 백업에서 직접 얻은 파일명과 위치**가 중요하며, 복원 서버에 이 복제 설정 명령을 따로 실행하는 절차가 아니다.

```text
-- CHANGE MASTER TO MASTER_LOG_FILE='pitr-bin.000004', MASTER_LOG_POS=197;
```

8.0의 버전과 도구에 따라 `SOURCE_LOG_FILE`·`SOURCE_LOG_POS` 명칭을 사용할 수도 있다. 물리 백업이라면 해당 도구가 보관한 좌표를 사용한다. mysqld 시작 로그에 보이는 마지막 InnoDB binlog 위치만으로 전체 백업의 경계를 확정하지 않는다. 그 값에 DDL이나 비InnoDB 변경이 모두 반영되었다고 보장할 수 없기 때문이다.

### 2.2 GTID와 데이터 범위를 구분한다

GTID는 트랜잭션 식별자이지 행 데이터의 체크섬이 아니다. `--set-gtid-purged=ON`이 기록하는 GTID 집합은 덤프한 데이터베이스만이 아니라 원본 서버 전체의 실행 이력을 반영한다. 따라서 일부 DB만 가져오면서 전체 원본 이력을 실행했다고 표시하는 부작용을 이해해야 한다.

이 글의 실험은 업무 스키마가 하나뿐인 폐기용 원본과 새 복원 대상에서 수행했다. 원본 이력을 보존한 덤프를 복원하고, 뒤의 binlog도 원래 GTID를 유지해 적용했다. 이미 다른 업무 데이터·GTID 이력이 있는 서버에 같은 절차를 그대로 적용하지 않는다. `gtid_purged` 충돌을 피하려고 임의로 이력을 초기화하거나 `--skip-gtids`를 붙이는 것은 복구 설계의 대체가 아니다. 향후 복제에 다시 참여시킬지, 독립 서버로 운영할지까지 먼저 결정해야 한다.

### 2.3 보존 기간은 백업 파일과 binlog를 묶어서 정한다

가장 오래 유지하는 복구 기준 백업부터 목표 시점까지 로그가 이어져야 한다. 서버의 자동 binlog 만료 기간만 늘리는 것으로 외부 아카이브의 내구성이 보장되지는 않는다. 파일명·크기·체크섬·원본 서버 식별자·백업 좌표를 함께 보관하고, 아카이브 지연과 누락을 별도로 감시한다. 백업 암호화 키를 사고 시점에 사용할 수 있는지도 복구 조건에 포함된다.

checksum은 파일의 전송·보관 무결성 확인에 유용하지만 파일 누락이나 업무 데이터의 정합성까지 증명하지는 않는다. 서로 다른 서버의 같은 파일명도 같은 이력을 뜻하지 않는다.

## 3. 사고 대응 순서: 보존, 경계 식별, 격리 복원

### 3.1 원본을 고치기 전에 증거를 보존한다

사고가 확인되면 추가 쓰기 차단 범위와 서비스 중단 여부를 결정하고, 관련 binlog와 백업을 보존한다. 로그 만료·수동 purge·정리 자동화로 필요한 파일이 지워지지 않게 한다. 쓰기를 계속 허용했다면 목표 경계 이후의 정상 거래를 어떻게 처리할지도 기록한다.

활성 binlog를 단순 파일 복사하면 복사 중 늘어난 부분이 누락되거나 끝 이벤트가 잘릴 수 있다. 적절한 권한과 운영 영향을 검토해 로그를 회전시키고 닫힌 파일을 확보하거나, 검증된 원격 binlog 아카이빙 절차를 사용한다. 실험에서는 필요한 파일을 회전으로 닫은 뒤 원본 바이트를 보관했다.

### 3.2 시간은 검색 범위, 위치는 적용 경계다

사고 시각을 알고 있다면 `mysqlbinlog --start-datetime`·`--stop-datetime`으로 주변을 조사할 수 있다. 이 시간은 mysqlbinlog 실행 호스트의 로컬 시간대로 해석되므로 UTC/KST, 애플리케이션 기록 시간, 원본 서버 시간을 먼저 맞춘다. 시간 필터만으로 최종 적용 범위를 정하지 말고 트랜잭션 단위의 파일·위치로 확정한다.

다음은 실험에서 확보한 두 파일을 **사람이 읽기 위한 형태**로 해석하는 명령이다. 파일명은 환경마다 다시 확인해야 한다.

```bash
mysqlbinlog --base64-output=DECODE-ROWS -vv \
  pitr-bin.000004 pitr-bin.000005 > inspect.txt
```

ROW 형식의 `### DELETE FROM` 같은 줄은 원래 SQL의 완전한 재현문이 아니라 행 이벤트를 읽기 쉽게 표시한 주석이다. 해당 행 이벤트 앞의 GTID/BEGIN부터 Xid/COMMIT까지 함께 조사한다. 삭제 앞 행 이벤트에서 멈추는 것과 사고 트랜잭션 전체를 제외하는 것은 다르다. 한 트랜잭션 안에 정상처럼 보이는 다른 UPDATE와 감사 INSERT가 함께 있을 수 있다.

`--base64-output=DECODE-ROWS` 출력은 복원에 필요한 `BINLOG` 문을 생략하므로 mysql에 공급할 재실행 파일로 사용하지 않는다. 조사 파일과 재실행 파일을 명확히 분리한다.

### 3.3 복원 대상에서 자동 동작을 막는다

복원 대상은 애플리케이션, 배치, 외부 전송 작업으로부터 격리한다. Event Scheduler와 복제 자동 시작 여부도 확인한다. 이벤트 정의를 정상 복원하는 것과 복원 도중 이벤트를 실행하도록 허용하는 것은 다른 문제다. 원본을 덮어쓰지 않고 새 대상을 사용하는 이유는 재시도 가능한 기준 상태와 원본 증거를 보존하기 위해서다.

데이터를 적재하기 전 버전, 문자셋·collation, `lower_case_table_names`, 플러그인·암호화 키, 저장 프로그램 DEFINER와 계정 정책을 점검한다. 특히 물리 백업은 해당 도구의 서버 버전·파일 형식 호환성 및 prepare 요건을 따른다.

## 4. 축소 재현: 정상 거래 두 건 뒤 전체 행 삭제

아래 SQL은 빈 실험 환경에서 순서대로 실행한다. 생성되는 `pitr_demo`는 폐기 전용 스키마다. SQL 문법과 실행 결과는 별도 MySQL 8.0.46 테스트에서도 확인했으며, **PITR 자체는 binlog를 활성화한 두 인스턴스 실험으로 따로 검증했다.**

### 4.1 기준 상태와 전체 백업

```sql
CREATE DATABASE pitr_demo;
CREATE TABLE pitr_demo.accounts (
    id INT PRIMARY KEY,
    balance INT NOT NULL
) ENGINE=InnoDB;
CREATE TABLE pitr_demo.audit (
    id INT PRIMARY KEY,
    action VARCHAR(32) NOT NULL
) ENGINE=InnoDB;
INSERT INTO pitr_demo.accounts VALUES (1,1000),(2,1000);
INSERT INTO pitr_demo.audit VALUES (1,'baseline');
SELECT id, balance FROM pitr_demo.accounts ORDER BY id;
```

실행 결과(MySQL 8.0.x):

```text
mysql> CREATE DATABASE pitr_demo;

Query OK, 1 row affected (0.00 sec)

mysql> CREATE TABLE pitr_demo.accounts (
    ->     id INT PRIMARY KEY,
    ->     balance INT NOT NULL
    -> ) ENGINE=InnoDB;

Query OK, 0 rows affected (0.00 sec)

mysql> CREATE TABLE pitr_demo.audit (
    ->     id INT PRIMARY KEY,
    ->     action VARCHAR(32) NOT NULL
    -> ) ENGINE=InnoDB;

Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO pitr_demo.accounts VALUES (1,1000),(2,1000);

Query OK, 2 rows affected (0.01 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql> INSERT INTO pitr_demo.audit VALUES (1,'baseline');

Query OK, 1 row affected (0.00 sec)

mysql> SELECT id, balance FROM pitr_demo.accounts ORDER BY id;

+----+---------+
| id | balance |
+----+---------+
|  1 |    1000 |
|  2 |    1000 |
+----+---------+
2 rows in set (0.00 sec)
```

이 시점에 앞 절의 전체 덤프를 만든다. 실제 PITR 실험에서는 백업 전에 로그를 회전했고, 덤프에 기록된 시작점은 `pitr-bin.000004:197`이었다. 이 숫자는 복사용 설정값이 아니라 한 번의 검증에서 얻은 값이다.

### 4.2 첫 번째 파일의 정상 이체

```sql
START TRANSACTION;
UPDATE pitr_demo.accounts SET balance=balance-100 WHERE id=1;
UPDATE pitr_demo.accounts SET balance=balance+100 WHERE id=2;
INSERT INTO pitr_demo.audit VALUES (2,'transfer');
COMMIT;
SELECT id, balance FROM pitr_demo.accounts ORDER BY id;
```

실행 결과(MySQL 8.0.x):

```text
mysql> START TRANSACTION;

Query OK, 0 rows affected (0.00 sec)

mysql> UPDATE pitr_demo.accounts SET balance=balance-100 WHERE id=1;

Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> UPDATE pitr_demo.accounts SET balance=balance+100 WHERE id=2;

Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> INSERT INTO pitr_demo.audit VALUES (2,'transfer');

Query OK, 1 row affected (0.00 sec)

mysql> COMMIT;

Query OK, 0 rows affected (0.00 sec)

mysql> SELECT id, balance FROM pitr_demo.accounts ORDER BY id;

+----+---------+
| id | balance |
+----+---------+
|  1 |     900 |
|  2 |    1100 |
+----+---------+
2 rows in set (0.00 sec)
```

이체는 두 잔액 변경과 감사 행을 하나의 트랜잭션으로 묶는다. 실제 PITR 실험에서는 이 커밋 뒤 로그를 회전해 다음 정상 거래가 다른 파일에 기록되도록 했다. 단순히 한 파일의 재실행만 성공하는지보다, 백업 좌표부터 파일 경계를 넘어 이력이 연결되는지를 확인하기 위한 구성이다.

### 4.3 두 번째 파일의 신규 계좌와 목표 경계

```sql
START TRANSACTION;
INSERT INTO pitr_demo.accounts VALUES (3,500);
INSERT INTO pitr_demo.audit VALUES (3,'new_account');
COMMIT;
SELECT id, balance FROM pitr_demo.accounts ORDER BY id;
SELECT id, action FROM pitr_demo.audit ORDER BY id;
```

실행 결과(MySQL 8.0.x):

```text
mysql> START TRANSACTION;

Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO pitr_demo.accounts VALUES (3,500);

Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO pitr_demo.audit VALUES (3,'new_account');

Query OK, 1 row affected (0.00 sec)

mysql> COMMIT;

Query OK, 0 rows affected (0.00 sec)

mysql> SELECT id, balance FROM pitr_demo.accounts ORDER BY id;

+----+---------+
| id | balance |
+----+---------+
|  1 |     900 |
|  2 |    1100 |
|  3 |     500 |
+----+---------+
3 rows in set (0.00 sec)

mysql> SELECT id, action FROM pitr_demo.audit ORDER BY id;

+----+-------------+
| id | action      |
+----+-------------+
|  1 | baseline    |
|  2 | transfer    |
|  3 | new_account |
+----+-------------+
3 rows in set (0.00 sec)
```

이 커밋 이후 전체 행을 별도 기준값으로 보관했다. 실제 원본의 다음 이벤트 위치는 `pitr-bin.000005:598`이었고, 뒤이어 발생시킨 사고 트랜잭션의 GTID 이벤트가 이 위치에서 시작하는지 로그를 대조했다. 따라서 복구 스트림은 첫 파일의 197부터 시작해 **마지막 파일의 598에서 시작하는 이벤트를 포함하지 않도록** 만들 수 있다.

### 4.4 사고 트랜잭션

아래는 오직 앞서 만든 폐기용 원본에서만 실행하는 삭제 재현이다. 사고 뒤에는 잔액 행이 없지만 `bad_delete` 감사 행이 남는다. 복구 시 둘 다 사고 트랜잭션 전체와 함께 제외되어야 한다.

```sql
START TRANSACTION;
DELETE FROM pitr_demo.accounts;
INSERT INTO pitr_demo.audit VALUES (4,'bad_delete');
COMMIT;
SELECT COUNT(*) AS account_rows FROM pitr_demo.accounts;
SELECT id, action FROM pitr_demo.audit ORDER BY id;
```

실행 결과(MySQL 8.0.x):

```text
mysql> START TRANSACTION;

Query OK, 0 rows affected (0.00 sec)

mysql> DELETE FROM pitr_demo.accounts;

Query OK, 3 rows affected (0.00 sec)

mysql> INSERT INTO pitr_demo.audit VALUES (4,'bad_delete');

Query OK, 1 row affected (0.00 sec)

mysql> COMMIT;

Query OK, 0 rows affected (0.01 sec)

mysql> SELECT COUNT(*) AS account_rows FROM pitr_demo.accounts;

+--------------+
| account_rows |
+--------------+
|            0 |
+--------------+
1 row in set (0.00 sec)

mysql> SELECT id, action FROM pitr_demo.audit ORDER BY id;

+----+-------------+
| id | action      |
+----+-------------+
|  1 | baseline    |
|  2 | transfer    |
|  3 | new_account |
|  4 | bad_delete  |
+----+-------------+
4 rows in set (0.00 sec)
```

## 5. 전체 백업 복원 후 연속된 로그를 한 세션에 적용한다

먼저 새 복원 서버에 `full.sql`을 적재한다. 실험에서는 이 단계의 모든 행이 백업 직전 기준값과 정확히 같은지 확인한 다음 binlog를 적용했다.

```bash
mysql --login-path=pitr-restore < full.sql

mysqlbinlog --start-position=197 --stop-position=598 \
  pitr-bin.000004 pitr-bin.000005 > replay.sql

mysql --login-path=pitr-restore < replay.sql
```

각 명령의 종료 코드가 0인지 확인한 뒤에만 다음 명령으로 진행한다. 스크립트라면 실패 즉시 중단하도록 구성하고, 파이프로 직접 연결할 때는 `pipefail` 등으로 앞단 mysqlbinlog 실패도 감지한다. `mysql --force`로 오류를 무시하며 계속 진행하지 않는다.

이 명령에서 `--start-position`은 첫 번째 파일, `--stop-position`은 마지막 파일에 적용된다. 파일 목록은 백업과 같은 원본 계열에서 얻은 로그를 순서대로 명시한다. 다른 서버 이력이 섞일 수 있는 무분별한 와일드카드 확장은 피한다. 두 파일 사이에 필요한 파일이 더 있다면 빠짐없이 포함해야 한다.

재실행에는 기본 base64 출력 동작을 유지해 ROW 이벤트를 실행 가능한 `BINLOG` 문으로 만든다. 여러 파일의 내용을 한 mysql 연결에서 실행하면 파일 사이의 세션 상태나 임시 테이블 의존성을 보존할 수 있다. 파일마다 별도의 mysql 연결로 나누어 실행하는 방식은 이런 의존성을 깨뜨릴 수 있다.

실패했다고 같은 스트림을 무조건 다시 실행하지 않는다. GTID 중복 방지가 일부 재적용을 막더라도 이미 실행된 DDL, GTID 처리 방식, 부분 적용 상태를 함께 판단해야 한다. 불명확하면 깨끗한 복원 대상에서 백업부터 다시 시작하는 편이 검증하기 쉽다.

### 실제 두 인스턴스 검증 결과

아래는 실험에서 확인한 전체 행과 assertion 결과를 읽기 쉽게 정리한 것이다. mysql-client 원문 화면이 아니라 검증 요약이며, 좌표와 결과를 임의로 만든 예시가 아니다.

| 점검 항목 | 확인 결과 |
|---|---|
| 원본·복원 서버 버전 | MySQL 8.0.46 |
| 백업 시작 좌표 | `pitr-bin.000004:197` |
| 사고 트랜잭션 시작 경계 | `pitr-bin.000005:598` |
| 재실행 파일 범위 | 연속된 binlog 두 개 |
| 전체 백업만 복원한 상태 | 두 테이블의 모든 행이 백업 기준값과 일치 |
| 사고 후 원본 계좌 행 | 0행 |
| PITR 후 계좌 전체 행 | `(1,900)`, `(2,1100)`, `(3,500)` |
| PITR 후 감사 전체 행 | `(1,baseline)`, `(2,transfer)`, `(3,new_account)` |
| 정상 경계까지의 원본 GTID 포함 여부 | 참 |
| 사고 트랜잭션 GTID 포함 여부 | 거짓 |
| 사고 직전 기준값과 복구 데이터 비교 | 두 테이블의 모든 행·모든 열 일치 |

계좌 행 수와 잔액 합계만 검사하면 잘못된 계좌별 배분을 놓칠 수 있다. 이 실험에서는 기본키로 정렬한 모든 행·모든 열을 비교했으며, 정상 GTID 포함 및 사고 GTID 부재도 각각 assertion으로 확인했다. GTID 확인은 데이터 비교를 보완하며 대체하지 않는다.

## 6. 운영 복구에서 자주 틀리는 판단

| 오해 또는 위험 | 운영상 올바른 판단 |
|---|---|
| 최신 백업을 무조건 고른다 | 목표 경계보다 뒤의 상태를 담은 백업은 binlog 전진 적용으로 과거로 되돌릴 수 없다 |
| 백업 종료 시각부터 로그를 적용한다 | 백업 도구가 기록한 일관된 좌표를 사용한다 |
| 삭제 행 이벤트 앞까지만 적용한다 | GTID/BEGIN/COMMIT을 포함해 사고 트랜잭션 전체의 경계를 조사한다 |
| 사고 트랜잭션만 빼고 뒤를 모두 적용한다 | 그것은 순수한 PITR이 아니며 후속 거래의 데이터 의존성을 검증해야 한다 |
| DB 필터로 복원 범위를 줄이면 안전하다 | `mysqlbinlog --database`의 판정은 로깅 형식과 기본 DB에 영향을 받는다. 교차 DB 트랜잭션을 잘라 적용하지 않는다 |
| 로그 파일 체크섬이 맞으면 복구가 끝났다 | 파일 연속성·적용 성공·업무 불변식·응용 동작을 별도로 확인한다 |
| 복원 서버가 켜지면 서비스를 전환한다 | 이전 writer 쓰기 차단, 연결 풀 재접속, 배치·이벤트 정책까지 확인한 뒤 승인한다 |

RPO는 백업 주기 하나로 정해지지 않는다. 아카이브된 마지막 binlog까지의 간격, 사고 경계 결정, 이후 정상 거래를 포기할지 보정할지가 함께 영향을 준다. RTO 역시 파일 복사 시간뿐 아니라 기준 백업 적재, 로그 재실행, 데이터 검증, 서비스 전환 시간을 합쳐 측정해야 한다. 이 소규모 기능 실험의 실행 시간을 운영 RTO로 환산하지 않는다.

## 7. Aurora MySQL에서는 관리형 PITR 절차를 사용한다

Aurora의 관리형 PITR은 사용자가 클러스터 저장소의 binlog 파일을 직접 추출해 이 절차대로 되돌리는 작업이 아니다. Aurora는 복구용 로그를 지속적으로 보관하며, 보존 기간 내 복원 가능한 시점을 지정해 **새 DB 클러스터**를 만든다. 외부 복제용 binlog 설정과 관리형 백업/PITR의 요건을 동일시하지 않는다.

목표 시점은 API가 반환하는 `EarliestRestorableTime`과 `LatestRestorableTime` 범위 안에 있어야 한다. 콘솔의 시간대 표시와 자동화 요청의 UTC 표기를 구분한다. 복원 후 적용할 VPC 보안 그룹, DB cluster/DB parameter group도 명시적으로 확인한다. 원본의 사용자 지정 파라미터가 항상 그대로 연결된다고 가정하지 않는다.

AWS CLI나 API로 클러스터를 시점 복원한 경우에는 writer DB 인스턴스를 별도로 생성해야 한다. 콘솔에서는 이 단계가 자동으로 처리된다. 새 클러스터와 writer가 준비되어도 애플리케이션 전환, 비밀정보·네트워크 접근, 외부 시스템 대조는 여전히 운영자의 책임이다. Aurora 복원은 이 글에서 실제 실행하지 않았으며, 여기의 두 컨테이너 결과를 Aurora 복구 성공으로 해석하면 안 된다.

## 8. 복구 승인 체크리스트

- [ ] 목표는 사고 직전 상태인가, 사고 이후 거래까지 보정한 상태인가? 포함·제외 범위를 승인받았는가?
- [ ] 목표보다 앞선 일관된 백업과 그 백업이 기록한 좌표·GTID 정보를 확보했는가?
- [ ] 필요한 모든 로그가 같은 원본 계열이며 파일 순서·누락·checksum을 확인했는가?
- [ ] GTID/BEGIN에서 COMMIT까지 조사해 목표가 트랜잭션 중간이 아님을 확인했는가?
- [ ] 시간대와 사고 시각 오차를 확인하고 최종 적용에는 파일·위치를 사용했는가?
- [ ] 복원 서버를 애플리케이션·복제·이벤트 실행으로부터 격리했는가?
- [ ] 백업 적재와 mysqlbinlog 생성 및 적용의 종료 코드·오류를 각각 확인했는가?
- [ ] 정상 거래·사고 거래의 포함 여부와 행·열·업무 불변식을 함께 검증했는가?
- [ ] 사고 이후 외부 결제·메시지·정상 거래의 대조와 보정 책임을 정했는가?
- [ ] 이전 writer를 확실히 차단하고 접속 전환 및 되돌리기 기준을 준비했는가?

## 정리

PITR의 성패는 mysqlbinlog 명령을 아는 것보다 **백업 기준점, 연속된 로그, 트랜잭션 단위의 중단 경계, 복구 후 검증**을 한 절차로 연결하는 데 달려 있다. 이번 실험에서는 전체 백업을 다른 인스턴스에 복원하고 두 로그 파일의 정상 거래만 적용해, 삭제 직전 두 테이블의 모든 행을 되찾았다. 다음 단계는 이 절차를 실제 데이터 규모와 권한·키·보존 정책에 맞춰 반복 가능한 복구 훈련으로 만드는 것이다.

## 참고 문서

- [MySQL 8.4: Point-in-Time Recovery](https://dev.mysql.com/doc/refman/8.4/en/point-in-time-recovery.html)
- [MySQL 8.4: 이벤트 위치를 이용한 시점 복구](https://dev.mysql.com/doc/refman/8.4/en/point-in-time-recovery-positions.html)
- [MySQL 8.4: mysqlbinlog 옵션과 재실행 주의사항](https://dev.mysql.com/doc/refman/8.4/en/mysqlbinlog.html)
- [MySQL 8.4: mysqldump 옵션](https://dev.mysql.com/doc/refman/8.4/en/mysqldump.html)
- [Amazon Aurora: 지정 시점으로 DB 클러스터 복원](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_PIT.html)
