Engineering Note
Active-Active DR은 Failover로 끝나지 않는다: 복구 검증, 데이터 조정, Failback
Active-Active DR에서 장애 전환 뒤 복구 기준점을 확정하고, 데이터 불일치를 조정하며, 안전하게 Failback하기 위한 운영 절차와 검증 기준을 설명합니다.
첫 번째 글에서는 Kafka 기반 Active-Active의 핵심이 클러스터 복제보다 쓰기 소유권이라고 설명했습니다. 두 번째 글에서는 그 소유권을 Home Region 쓰기 경로에 어떻게 강제할지 살펴봤습니다.
이제 리전 A에 실제 장애가 났다고 가정해보겠습니다.
리전 B의 애플리케이션은 살아 있고, 읽기 요청도 처리합니다. 운영자가 트래픽을 B로 돌리고 쓰기 소유권을 넘겼습니다. 대시보드는 다시 초록색입니다.
그렇다면 복구가 끝난 것일까요?
- A의 마지막 DB 커밋은 B에 도착했는가?
- Kafka 복제 지연이 아니라 B의 업무 DB가 실제로 반영한 지점은 어디인가?
- 타임아웃된 결제 요청은 실패했는가, 성공했는가?
- 복구된 A를 언제 다시 신뢰할 수 있는가?
- A로 돌아갈 때 예전 소유권 epoch를 재사용해도 되는가?
이 질문에 답하지 못하면 failover는 끝났어도 recovery는 끝나지 않았습니다.
Failover는 쓰기 위치를 바꾼다.
Recovery는 어떤 데이터까지 살아남았는지 증명한다.
Reconciliation은 남은 불확실성을 처리한다.
Failback은 다음 소유권 이전으로 수행한다.
이 글은 Active-Active DR 시리즈의 마지막 글로, 장애 감지부터 데이터 조정과 원복까지 운영 절차를 닫는 방법을 다룹니다.
여기서 failover는 꺼져 있던 대기 리전을 켠다는 뜻이 아닙니다. 양쪽 리전은 평소에도 트래픽을 처리하지만 같은 ownership key에는 하나의 writer만 존재합니다. 장애가 나면 영향받은 key의 쓰기 소유권과 실패한 리전이 감당하던 부하를 생존 리전으로 옮깁니다.
트래픽 전환과 쓰기 소유권 이전은 다른 상태다
글로벌 로드 밸런서는 사용자 요청이 어느 리전으로 들어갈지 결정합니다. 그러나 특정 ownership key를 어느 리전이 변경할 수 있는지는 소유권 저장소와 쓰기 경계가 결정합니다.
traffic route: Region A + Region B -> Region B only
write owner: Region A · epoch 42 -> FENCED -> Region B · epoch 43
두 상태가 항상 동시에 바뀌는 것은 아닙니다. health check가 A를 제외해도 소유권이 아직 A라면 B는 쓰기를 임의로 허용하면 안 됩니다. 반대로 소유권이 B로 이전됐지만 일부 요청이 A에 도착한다면 A는 새 writer가 아니라 forwarding 또는 rejection 지점이어야 합니다.
따라서 DR 자동화는 트래픽 상태와 소유권 상태를 별도로 기록하고, 새 리전의 쓰기 성공뿐 아니라 이전 epoch의 쓰기 거절까지 확인해야 합니다.
전환 성공과 복구 성공을 구분한다
글로벌 로드 밸런서가 트래픽을 B로 보냈다는 사실은 네트워크 경로가 바뀌었다는 뜻입니다. 새 리전에서 몇 건의 요청이 성공했다는 사실도 전체 서비스와 데이터가 복구됐다는 증거는 아닙니다.
DR 성공 조건은 최소한 다음 다섯 영역을 함께 봐야 합니다.
| 영역 | 답해야 할 질문 | 필요한 증거 |
|---|---|---|
| 가용성 | 핵심 기능이 목표 시간 안에 다시 열렸는가? | 최초 사용자 영향 또는 장애 시작부터 검증된 서비스 재개까지의 실제 시간 |
| 소유권 안전성 | 이전 리전이 더 이상 쓸 수 없는가? | 현재 owner와 epoch, stale writer 거절, DB 또는 쓰기 경계의 fencing 상태 |
| 데이터 복구 | 새 리전이 어느 지점까지 연속적으로 반영했는가? | Outbox, 리전 간 복제, Inbox, 업무 DB의 연속된 checkpoint |
| 용량과 의존성 | 새 리전이 전체 부하와 필수 외부 기능을 감당하는가? | 부하 테스트, 인증·KMS·DNS·결제 같은 hard dependency의 실제 호출 |
| 안정화 | 누락·중복·충돌·결과 불명 요청을 분류했는가? | reconciliation ledger, 처리 결과, 승인과 감사 기록 |
RTO는 “언제 트래픽을 돌렸는가”가 아니라 필수 업무 기능이 검증된 상태로 돌아올 때까지의 시간으로 측정해야 합니다. RPO도 “복제 지연이 평소 3초였다” 같은 설정값이 아니라 사고에서 실제로 확인된 복구 기준점으로 계산해야 합니다.
Google Cloud의 DR planning guide는 RTO와 RPO를 측정 가능한 목표로 두고, RPO가 시간 범위를 나타낼 뿐 유실된 데이터의 양이나 품질까지 설명하지는 않는다고 구분합니다. 따라서 결제 세 건과 캐시 갱신 세 건이 같은 “3초” 안에 있어도 업무 영향은 다를 수 있습니다.
Failover는 상태 기계로 실행한다
장애 전환을 명령어 목록으로만 적으면 어떤 조건에서 다음 단계로 넘어갈 수 있는지 알기 어렵습니다. 소유권과 데이터 상태를 함께 움직이는 상태 기계로 보는 편이 안전합니다.
각 단계는 다음 질문에 답해야 합니다.
- DETECT: 단일 health check 실패가 아니라 사용자 오류, 리전 의존성, 데이터 계층 상태를 함께 보고 장애 후보를 식별했는가?
- DECLARE: 전체 리전, 특정 서비스, 특정 ownership key 중 전환 범위를 정했고 계획 전환인지 강제 전환인지 기록했는가?
- FENCE A: 이전 credential 회수, DB read-only, lease 만료, 조건부 epoch 검증 중 실제 쓰기 경계에서 동작하는 차단 수단이 있는가?
- CHECKPOINT: B가 연속적으로 적용한 마지막 version과 알 수 없는 구간을 구분했는가?
- ACTIVATE B: 새 epoch로 소유권을 변경하고 작은 범위부터 쓰기를 검증했는가?
- RECONCILE: 누락, 중복, 충돌, 외부 side effect를 분류했는가?
- STABLE B: 실제 RTO와 RPO를 기록하고 전체 부하에서 안정 상태를 확인했는가?
- PLAN FAILBACK: 복구된 A를 새 replica처럼 다시 만들고 별도의 계획 이전을 준비했는가?
계획 전환이라면 FENCED(A) -> CHECKPOINT -> ACTIVE(B)를 순서대로 증명할 수 있습니다. 그러나 A가 완전히 단절된 강제 전환에서는 fencing이나 마지막 커밋을 확인하지 못할 수 있습니다.
이때 “확인할 수 없음”을 “문제없음”으로 바꾸면 안 됩니다. mode=forced, rpo_status=unknown처럼 불확실성을 상태에 남기고, 정합성을 위해 쓰기를 계속 막을지 정의된 데이터 위험을 승인하고 B를 열지 결정해야 합니다.
모든 장애를 Failover로 해결하지 않는다
리전 인프라가 단절됐거나 현재 writer가 업무 요청을 처리할 수 없다면 소유권 전환이 적절한 복구 수단일 수 있습니다. 하지만 원인이 잘못된 배포, 대량 삭제, 데이터 손상이라면 리전 복제는 그 문제도 빠르게 반대편으로 전달할 수 있습니다.
regional outage -> 이전 writer fencing + 소유권 전환
logical corruption -> 영향 쓰기 동결 + 정상 복구 시점 선택 + restore/replay
credential compromise -> 접근 차단 + credential 회전 + 감사와 범위 확인
논리적 손상인데 B를 곧바로 활성화하면 같은 손상된 상태를 더 높은 처리량으로 서비스할 수 있습니다. 이 경우에는 영향 범위의 쓰기를 먼저 동결하고, versioned backup이나 point-in-time recovery로 정상 지점을 복구한 뒤 검증된 이벤트만 재생해야 합니다.
AWS Well-Architected의 DR 전략 지침도 지속 복제가 일부 장애에는 유용하지만 데이터 손상이나 파괴까지 막지는 못하므로 versioning과 point-in-time recovery를 함께 준비해야 한다고 설명합니다. 상태 기계의 DECLARE 단계에는 전환 범위뿐 아니라 incident_kind도 남겨야 합니다.
복구 기준점은 Kafka lag 하나로 정할 수 없다
Home Region의 일반적인 변경은 다음 경로를 지납니다.
Region A DB commit
-> Outbox record
-> Kafka publish
-> cross-region replication
-> Region B Inbox
-> Region B business DB apply
이 경로에서 Kafka consumer lag가 0이라고 해도 복구가 끝났다고 단정할 수 없습니다.
- Outbox relay가 멈췄다면 아직 Kafka에 들어오지 않은 DB 변경이 있을 수 있습니다.
- 리전 간 복제가 source와 단절됐다면 target lag는 더 이상 증가하지 않지만 새 이벤트가 없다는 뜻은 아닙니다.
- Kafka에는 도착했어도 B의 Inbox나 업무 DB 적용이 밀려 있을 수 있습니다.
- 파티션 offset은 전달 위치를 나타내지만 업무 엔티티의 version이 연속적인지는 별도로 확인해야 합니다.
따라서 복구 기준점은 각 파이프라인의 숫자를 단순히 나열하는 대신, B의 업무 DB에 연속적으로 적용되고 검증된 가장 높은 업무 version으로 표현하는 편이 좋습니다.
published watermark = A의 Outbox에서 Kafka로 확인된 마지막 지점
replicated watermark = B의 Kafka에 확인된 마지막 지점
applied watermark = B의 Inbox와 업무 DB에 원자적으로 반영된 마지막 연속 지점
recovery checkpoint = B에서 업무적으로 검증된 applied watermark
모든 엔티티를 하나의 전역 번호로 만들 필요는 없습니다. aggregate_id나 ownership key별 version, Kafka partition별 offset, 이벤트 발생 시각을 함께 보관할 수 있습니다. 중요한 것은 장애 시점에 “어떤 단위가 어디까지 안전한가”를 다시 계산할 수 있는 것입니다.
A가 복구되기 전에는 source DB에만 남은 마지막 커밋이 있는지 알 수 없을 수 있습니다. 이 경우 checkpoint는 “B가 안전하게 가진 마지막 지점”이지 “그 이후 유실이 정확히 0건”이라는 증명은 아닙니다. 실제 RPO는 A의 잔여 데이터와 로그를 회수해 비교한 뒤 확정하거나, 끝내 회수할 수 없다면 검증 불가 범위로 남겨야 합니다.
Runbook은 명령어보다 상태와 증거를 남겨야 한다
좋은 runbook은 “DNS를 바꾼다” 같은 문장만 나열하지 않습니다. 현재 상태, 전환 조건, 실행 결과, 승인자를 기계가 읽을 수 있는 형태로 남깁니다.
incidentId: inc-2026-0716-01
incidentKind: regional-outage
scope:
type: ownership-key-range
value: tenant-0001..tenant-2499
mode: forced
sourceRegion: region-a
targetRegion: region-b
fromEpoch: 42
toEpoch: 43
oldOwnerFenced: unconfirmed
recoveryCheckpoint:
type: aggregate-version
value: order:812
rpoStatus: unknown
approval:
dataRiskAcceptedBy: incident-commander
approvedAt: 2026-07-16T07:18:22Z
state: ACTIVATE_B
각 단계는 재실행해도 같은 결과가 나오는 idempotent 작업이어야 하고, 상태 변경은 compare-and-set으로 이전 단계를 확인해야 합니다. 작업자 노트와 스크립트 출력만 남기지 말고 소유권 레코드, checkpoint, 검증 결과를 하나의 incident record에 연결해야 합니다.
자동화의 범위도 구분할 필요가 있습니다.
- 장애 신호 수집, 의존성 점검, checkpoint 계산, 대상 용량 확인은 자동화하기 좋습니다.
- 이미 승인된 계획 전환의 단계 실행도 자동화하기 좋습니다.
- fencing과 RPO를 증명하지 못한 강제 전환은 데이터 유실 위험이 있으므로 명시적인 승인 gate가 필요합니다.
- 자동화가 예상과 다르게 움직일 때 즉시 중단할 수 있는 abort가 있어야 합니다.
AWS Well-Architected의 recovery automation 지침도 복구 절차를 관찰 가능하고 반복 가능하게 자동화하되, 데이터 손실 위험이 큰 상황을 무조건 자동 실행하거나 중단 수단 없이 운영하는 방식을 피하라고 설명합니다.
새 리전은 한 번에 전부 열지 않는다
리전 B가 평소 절반의 읽기 트래픽을 처리했다고 해서 A의 쓰기와 전체 부하까지 감당할 수 있다는 뜻은 아닙니다. 애플리케이션 replica 수뿐 아니라 다음 경계를 확인해야 합니다.
DB connection과 write IOPS
Kafka producer와 consumer 처리량
Outbox와 Inbox backlog 소진 속도
cache warm-up과 rate limit
KMS, Secret, 인증과 권한
결제, 이메일, object storage 같은 외부 의존성
운영자와 배포 파이프라인의 B 리전 접근 권한
가능하다면 ownership key의 작은 범위부터 B의 새 epoch로 활성화합니다. synthetic write 한 건이 성공했다는 데서 멈추지 말고 다음 결과까지 확인합니다.
- B의 DB와 Outbox가 같은 트랜잭션으로 커밋됐다.
- 이벤트가 B의 Kafka와 필요한 리전 간 경로에 발행됐다.
- Inbox와 projection이 예상 version을 적용했다.
- 다시 읽은 결과가 업무 불변식과 맞는다.
- 같은 idempotency key를 재시도해도 side effect가 한 번만 발생한다.
- A의 오래된 epoch 요청이 계속 거절된다.
그다음 ownership key와 트래픽 비율을 단계적으로 늘립니다. 이 과정에서 오류율만 보지 말고 stale epoch rejection, Outbox oldest age, Inbox apply lag, DB saturation, 외부 API 실패를 함께 봐야 합니다.
Failover 뒤에는 데이터 조정 큐가 생긴다
강제 전환은 불확실성을 없애는 작업이 아니라 더 이상 커지지 않도록 경계를 긋는 작업입니다. A가 복구되면 그 안의 데이터를 B에 통째로 덮어쓰지 말고 증거로 가져와 비교해야 합니다.
분류 기준은 다음처럼 잡을 수 있습니다.
| 분류 | 판단 예시 | 처리 원칙 |
|---|---|---|
| Duplicate | 같은 event_id와 payload hash가 B의 Inbox에 이미 있음 | 다시 적용하지 않고 비교 결과만 기록 |
| Missing | aggregate version이 41, 42, 44처럼 연속되지 않음 | 원본 이벤트를 확보해 idempotent하게 재생하고 불변식 검증 |
| Conflict | 같은 aggregate version인데 payload hash나 업무 상태가 다름 | last-write-wins로 숨기지 말고 격리 후 업무 규칙으로 결정 |
| Side effect | 결제 승인이나 이메일 발송 결과가 로컬 DB만으로 확정되지 않음 | 외부 시스템의 idempotency key와 상태를 조회한 뒤 재시도 또는 보상 |
특히 이전 epoch에서 발견된 DB row를 현재 B에 그대로 복사하면 안 됩니다. 그 row가 누락된 정상 커밋인지, fencing 이후 발생한 stale write인지, 이미 다른 이벤트로 대체된 상태인지 먼저 판정해야 합니다.
reconciliation ledger에는 최소한 event_id, ownership key, aggregate version, owner epoch, payload hash, 양쪽 상태, 분류 결과, 실행한 replay 또는 compensation, 승인자를 남깁니다. 그래야 실제 데이터 유실량과 처리 결과를 사고 보고서에 연결할 수 있습니다.
Failback은 되돌리기가 아니라 새 소유권 이전이다
리전 A가 다시 켜졌다고 해서 자동으로 원래 역할을 돌려주면 안 됩니다. 강제 전환 뒤에는 B가 현재 source of truth이고, A는 뒤처진 replica입니다.
안전한 failback은 다음 순서에 가깝습니다.
1. 복구된 A의 쓰기 경로를 계속 차단한다.
2. B의 snapshot과 이벤트로 A를 다시 구성한다.
3. A가 B의 checkpoint까지 연속적으로 따라왔는지 검증한다.
4. reconciliation 미처리 항목이 전환 기준 아래인지 확인한다.
5. B -> A 계획 소유권 이전을 새로 시작한다.
6. B를 fencing하고 A에 새 epoch 44를 발급한다.
7. 작은 범위부터 A를 활성화하고 다시 안정 상태를 증명한다.
A가 과거에 사용하던 epoch 42를 재사용하면 안 됩니다. B에서 epoch 43으로 처리된 요청과 지연된 A의 요청을 구분할 수 없기 때문입니다. 원복도 단조 증가하는 새 epoch를 가진 별도의 전환입니다.
두 리전이 대칭이고 B가 정상 운영을 계속할 수 있다면 즉시 A로 돌아갈 필요가 없을 수도 있습니다. “원래 primary니까”가 아니라 비용, 데이터 지역성, 지연, 다음 장애 대비 용량을 근거로 failback 필요성을 결정합니다.
Azure Well-Architected의 DR 지침은 failback을 failover와 구분된 절차로 설계하고, 같은 수준의 계획과 검증을 적용하라고 권고합니다. 원복을 역순 명령 실행으로 취급하지 말아야 하는 이유입니다.
DR Drill은 아키텍처가 아니라 복구 능력을 측정한다
DR 문서가 존재하는 것과 팀이 목표 시간 안에 복구할 수 있는 것은 다릅니다. AWS Well-Architected의 DR test 지침도 recovery site로 실제 전환해 RTO와 RPO가 충족되는지 정기적으로 검증해야 한다고 설명합니다.
한 가지 “리전 종료” 시나리오만 반복하지 말고, 설계가 어려워하는 경계를 나눠 연습하는 편이 좋습니다.
| Drill 시나리오 | 반드시 확인할 결과 |
|---|---|
| 리전 A 전체 단절 | B가 A의 control plane 없이도 활성화되고 필수 credential과 runbook에 접근할 수 있음 |
| A와 B 사이 network partition | 두 리전이 동시에 같은 ownership key를 쓰지 못하고 stale epoch가 거절됨 |
| Ownership Control Plane 장애 | 기존 owner 유지 또는 fail-closed 정책이 의도대로 동작함 |
| Outbox 또는 리전 간 복제 지연 | Kafka lag 하나가 아니라 실제 applied checkpoint로 전환 가능 여부를 판단함 |
| 논리적 데이터 손상 | 리전 전환을 서두르지 않고 정상 복구 시점을 선택해 restore와 검증된 replay를 수행함 |
| B의 부분 의존성 장애 | 인증, KMS, 결제 같은 hard dependency 실패를 health check가 숨기지 않음 |
| B의 전체 부하 수용 | DB, Kafka, cache, 외부 API가 목표 처리량에서 안정적임 |
| Failback | B를 현재 권한으로 두고 A를 재구성한 뒤 새 epoch로 계획 이전함 |
연습은 tabletop, 비운영 환경 dry run, 제한된 production ownership key 전환, 전체 전환처럼 위험 수준을 나눌 수 있습니다. 중요한 것은 현실적인 경로를 한 번도 실행하지 않은 채 문서만 읽고 성공으로 처리하지 않는 것입니다.
각 drill이 끝나면 다음 수치를 남깁니다.
장애 감지 시간
DR 선언과 승인 시간
이전 owner fencing 완료 시간
checkpoint 확정 시간과 근거
첫 검증 쓰기 성공 시간
전체 부하 전환 시간
실제 RTO와 실제 RPO
누락·중복·충돌·결과 불명 건수
수동 단계 수와 실패한 자동화
안정 상태와 failback 완료 시간
구조나 의존성이 크게 바뀌면 기존 drill 결과를 현재 복구 능력으로 간주하지 말고 다시 검증해야 합니다.
운영 화면은 복구 상태를 한 흐름으로 보여줘야 한다
장애 중에는 Kafka 대시보드, DB 콘솔, 로드 밸런서, 소유권 저장소를 따로 열어 숫자를 머릿속에서 조합할 시간이 부족합니다. 최소한 다음 정보를 incident와 ownership scope 기준으로 연결해 볼 수 있어야 합니다.
- 현재 recovery state와 마지막 전이 시각
- source와 target region, owner epoch, fencing 증거
- Outbox oldest age, publish와 cross-region replication watermark
- Inbox backlog와 업무 DB applied checkpoint
- target region의 용량과 hard dependency 상태
- canary 결과, stale writer 거절, 결과 불명 요청
- reconciliation 분류와 미처리 건수
- 승인자, 자동화 실행 이력, abort와 재시도 기록
이런 관점에서 운영 도구는 개별 리소스가 “정상”이라고 표시하는 데서 끝나지 않고, 복구 판단에 필요한 근거를 하나의 작업 흐름으로 묶어야 합니다. Konduo도 리소스 상태와 메트릭·로그 근거, 진단, 알림 대응, 운영 이력을 연결하는 통합 관리 운영 플랫폼을 지향합니다.
마지막 점검 체크리스트
- DR scope를 서비스 전체가 아니라 ownership key나 업무 경계 단위로도 정의할 수 있는가?
- 트래픽 라우팅 상태와 쓰기 소유권 상태를 별도로 기록하고 검증하는가?
- 장애 원인이 리전 단절, 논리적 손상, credential 침해 중 무엇인지 선언하는가?
- RTO의 종료 조건이 “트래픽 변경”이 아니라 “핵심 기능 검증 완료”로 정의되어 있는가?
- RPO를 Outbox, Kafka, Inbox, 업무 DB의 end-to-end checkpoint로 계산할 수 있는가?
- source가 단절됐을 때
unknown을 표현하고 데이터 위험 승인자를 남길 수 있는가? - 이전 owner를 우회 불가능한 쓰기 경계에서 fencing할 수 있는가?
- 강제 전환 자동화에 승인 gate와 abort가 있는가?
- target region이 전체 부하와 hard dependency를 감당하는지 실제로 검증했는가?
- ownership key 일부부터 canary 전환할 수 있는가?
- 누락, 중복, 충돌, 외부 side effect를 분류하는 reconciliation ledger가 있는가?
- 복구된 이전 리전의 데이터를 현재 owner에 그대로 덮어쓰지 않는가?
- 복제된 데이터도 손상될 수 있다는 전제에서 versioned backup과 PITR을 실제로 시험했는가?
- failback을 새 epoch를 발급하는 별도 계획 이전으로 실행하는가?
- 실제 RTO, RPO와 데이터 조정 결과를 남기는 DR drill을 반복하고 있는가?
결론
Active-Active DR은 두 리전에 애플리케이션과 Kafka를 배치했다고 완성되지 않습니다. Home Region 쓰기를 구현하고 새 리전으로 트래픽을 돌린 뒤에도 운영 과제가 남습니다.
이전 writer가 실제로 차단됐는지 증명해야 합니다. 새 리전이 어느 데이터까지 연속적으로 반영했는지 선언해야 합니다. 누락과 중복, 충돌과 외부 side effect를 조정해야 합니다. 복구된 리전으로 돌아갈 때도 과거 상태를 되살리는 대신 새 소유권 이전을 수행해야 합니다.
Failover는 시작점이다.
검증된 checkpoint와 reconciliation이 recovery를 완성한다.
새 epoch를 가진 failback이 다음 정상 상태를 만든다.
이렇게 보면 Active-Active DR의 마지막 산출물은 화려한 멀티 리전 구성도가 아닙니다. 팀이 반복해서 실행하고 측정할 수 있는 복구 상태 기계와, 데이터 불확실성을 숨기지 않는 운영 기록입니다.
함께 읽기 좋은 글
- Kafka Active-Active의 핵심은 Kafka가 아니라 쓰기 소유권이다 - 시리즈의 출발점으로 Active-Active에서 먼저 정해야 할 쓰기 소유권 모델을 설명합니다.
- 멀티 리전 서비스의 Home Region 쓰기 - 소유권 판정부터 DB, Outbox, Kafka와 epoch fencing까지 실제 쓰기 경로로 연결합니다.
- Kafka는 왜 Outbox와 Inbox 사이에 잘 맞을까 - 데이터 조정의 기반이 되는 producer와 consumer의 멱등 계약을 자세히 다룹니다.