[DR] DR 아키텍처, 왜 이렇게 설계했나.. (DR 테스트 글 후속편)
지난 글(서울 리전 장애 유발로 도쿄 리전 활성화 하기)에서는 실제로 서울 리전에 장애를 일으켜보고 도쿄로 전환되는 과정을 지켜본 페일오버 테스트 결과를 정리했다. 이번 글은 그 결과물이 "무엇으로 이루어져 있는가"가 아니라 "왜 하필 이런 구성이었는가"를 다룬다. 돌아보면 이 DR 구성의 서비스 하나하나는 따로따로 고른 게 아니었다. 앞 단계에서 생긴 문제를 풀다 보니 다음 서비스가 자연스럽게 필요해지는 흐름이었다. 서울이 통째로 죽을 수 있다는 문제가 도쿄에 사본을 두자는 결론으로 이어지고, 사본이 생기니 트래픽을 어떻게 그쪽으로 보낼지가 문제가 되고, 자동 전환을 만들다 보니 딱 한 곳(DB 승격)만 사람 손을 거치게 남겨야 했고, 그 수동 판단이 실제 테스트에서 발목을 잡는 걸 보고서야 "판단을 도와주는 자동화"가 필요해졌다. 이 글은 그 흐름을 5단계로 나눠 살펴본다. 1단계 — "다른 리전에 사본이 있어야 한다" 문제: 서울(apnortheast2) 리전이 AZ 장애 확산이나 네트워크 단절 같은 이유로 통째로 마비될 수 있다. 단일 리전 구조에서는 이 순간 서비스가 전면 중단된다. 결론: 물리적으로 격리된 다른 리전에 실행 가능한 사본을 둬야 한다 — 도쿄(apnortheast1)리전을 선택했다. 여기서부터 "서울에 있는 구성요소 하나하나를 도쿄에 어떻게 복제할까"라는 질문이 이어진다. 그리고 데이터의 성격(상태 데이터인가, 언제든 다시 만들 수 있는 파생 데이터인가)에 따라 복제 방식이 갈린다는 게 이 단계의 핵심 판단이다. 서울 구성요소 복제가 필요한 이유 선택된 서비스 왜 이 서비스인가 앱 실행 환경 서울과 동일한 방식으로 돌아가야 배포 파이프라인을 이원화하지 않는다 EKS 서울도 EKS이므로 같은 Helm 차트·같은 매니페스트 구조를 그대로 재사용 가능 트래픽 진입점 사용자 요청을 받아 파드로 연결 ALB HTTPS 종료 + K8s Target Group과 자연스럽게 연동 데이터베이스 DB는 "상태"라서 단순 스냅샷이 아니라 계속 최신을 따라가야 하고, 필요 시 독립 DB로 전환 가능해야 함 RDS CrossRegion Read Replica 지속적 복제 + 승격(promote) 한 번으로 독립 primary 전환이 가능한 구조가 요구에 정확히 부합 이미지·정적 파일 마찬가지로 상태 데이터 S3 리전 간 복제(CRR) S3가 리전 간 복제를 자체 기능으로 이미 제공 — 별도 구축 불필요 컨테이너 이미지 도쿄에서도 이미지를 pull 할 수 있어야 파드가 뜸 ECR 리전 간 복제 위와 동일한 논리 — 레지스트리 자체 복제 기능 활용 캐시 캐시는 상태가 아니라 언제든 재생성 가능한 파생 데이터 ElastiCache — 의도적으로 복제 안 함 상시 복제 비용을 낼 가치가 없다는 비용 대비 효익 판단 이 단계의 원칙을 한 줄로 줄이면 이렇다. 상태를 가진 것은 전부 복제하고, 언제든 다시 만들 수 있는 것(캐시)은 복제하지 않는다. 이 구분이 이후 모든 복제 전략의 비용 판단 기준이 된다. 2단계 — "사본은 있는데, 사용자를 어떻게 그리로 보내나" 문제: 도쿄에 동일한 인프라가 있어도, 사용자 요청이 자동으로 그쪽으로 흐르지 않으면 무용지물이다. 결론: DNS 레벨에서 "서울 응답 없음 → 도쿄로" 자동 전환이 필요하다. 구성요소 역할 Route 53 Failover Routing 도메인의 Primary는 서울, Secondary는 도쿄로 설정. 헬스체크 실패 시 자동으로 응답을 도쿄 ALB로 전환 Route 53 헬스체크 서울 ALB가 살아있는지 주기적으로 확인 CloudWatch 알람 + 알림 헬스체크 실패를 사람에게도 알림 — 자동 전환은 일어나지만, 그 사실을 사람이 인지해야 다음 판단(DB 승격 등)을 시작할 수 있음 왜 이 구간만 "완전 자동화"로 설계됐는가? 3단계에서 다룰 DB 승격과 달리, 이 구간은 유일하게 사람 개입 없이 전부 자동으로 설계된 부분이다. 이유는 명확하다: DNS 응답을 바꾸는 행위는 되돌리기 쉽고(가역적), 데이터를 전혀 건드리지 않는다. 잘못 전환돼도 다시 정상으로 돌아가면 그만이라 리스크가 낮다 — 그래서 자동화의 비용 대비 이득이 확실한 구간이다. 3단계 — DB 승격만 수동으로 남긴 이유, 그리고 그것이 낳은 문제 초기 설계는 DB 승격(promote)을 명시적으로 "수동, 운영자 판단 후 실행" 으로 못박았다. 인프라와 트래픽은 자동화하면서 이 한 지점만 수동으로 남긴 이유는, 2단계와 정반대의 리스크 구조 때문이다. Route53 트래픽 전환 (자동) DB 승격 (수동으로 시작) 되돌릴 수 있는가 가역적 — DNS 응답만 바뀜 비가역 — 한 번 승격하면 되돌릴 수 없음 잘못되면 일시적 오탐, 다시 정상 응답하면 자동 복구 splitbrain — 서울·도쿄가 동시에 쓰기 가능한 상태가 되어 데이터 정합성이 깨짐 설계 원칙 "빠른 게 최우선" "틀리지 않는 게 최우선" 그런데 "수동"이 새로운 문제를 낳았다 "수동"이라는 말은 곧 내가 항상 판단할 준비가 되어 있어야 한다는 뜻이었다. 이건 막연한 걱정이 아니라 실제 페일오버 테스트를 해보면서 몸으로 느낀 문제였다. 트래픽이 도쿄로 넘어간 시점과 내가 실제로 DB 승격 명령을 누른 시점 사이에 약 2분의 공백이 있었는데, 그 사이에 들어온 쓰기 요청은 도쿄 RDS가 아직 read replica라 실패했을 수 있겠다 싶었다. "판단은 도와주되, 거기 걸리는 시간은 줄이고 싶다" — 이 생각이 다음 단계에서 다룰 승인 자동화 워크플로를 만들게 된 직접적인 계기였다. 테스트가 끝나자마자 바로 이 워크플로를 만들기 시작했다. 4단계 — "판단은 돕되, 방아쇠는 사람이 당긴다" 3단계의 요구를 그대로 풀어보면 이렇다. "사람이 판단할 재료(위험도 해석)는 자동으로 마련해주되, 최종 실행(승격)의 방아쇠는 여전히 사람이 당기게 한다." 이 요구 하나에서 아래 서비스 선택이 전부 파생된다. ① 서버리스(Lambda) — 왜 상시 서버가 아닌가.. 이 파이프라인은 평소엔 1분에 한 번 상태를 확인하는 것 외엔 거의 동작하지 않는다. 장애라는 이벤트가 터질 때만 깨어나면 되는 워크로드라, 상시 서버를 띄워두면 대부분의 시간을 낭비하게 된다. 기능을 진단·탐지·차단·승격·전환·검증·검수 등 여러 개의 작은 단위로 쪼갠 이유도 있다 — 각 단위에 필요한 최소 권한만 부여하기 위해서다. 예를 들어 "차단" 역할은 DB 접근 제어 권한만, "승격" 역할은 DB 승격 권한만 갖는다 — 하나가 뚫려도 피해가 그 역할의 권한 범위 안으로 제한된다. ② 워크플로 오케스트레이션(Step Functions) — 왜 함수끼리 직접 호출하지 않았나.. 문제 1 — 긴 대기: 사람 승인을 최대 15분까지 기다려야 하는데, 서버리스 함수는 실행 시간 제한이 있고 대기 상태를 함수 자신이 붙들고 있으면 그 시간만큼 과금된다. 해결: 워크플로가 토큰을 발급하고 실행을 "일시정지" 상태로 두면, 외부 이벤트(Slack 버튼 클릭)가 토큰을 반납할 때까지 과금 없이 대기할 수 있는 패턴을 활용했다. 문제 2 — 회고 데이터 확보: "복구까지 몇 분 걸렸나"를 측정하려면 각 단계 소요시간을 어딘가에 기록해야 한다. 해결: 워크플로 오케스트레이션 서비스는 상태 전환마다 자동으로 타임스탬프를 실행 기록에 남긴다. 별도 계측 코드 없이 이 기록을 읽기만 하면 되므로, 사후 회고 보고서가 거의 공짜로 구현될 수 있었다. ③ 별도 상태 저장소(DynamoDB) — 왜 RDS가 아니라 별도 저장소인가.. 문제: "지금 진짜 주인(primary)이 어디냐"를 기록할 저장소가 필요하다. 그런데 이걸 RDS(서울 혹은 도쿄)에 기록하면, 장애의 당사자가 될 수도 있는 그 DB에, 장애 상태를 기록하는 시스템이 의존하는 순환 논리가 생긴다. 해결: RDS와 완전히 독립된, 별도로 가용성이 높은 저장소가 필요했다. 선택된 서비스는 서버리스·고가용성이면서 조건부 쓰기(동시에 같은 값을 쓰려는 시도 중 하나만 성공시키는 방식)를 기본 지원한다. 이게 "동시에 두 곳에서 같은 승격을 시도하지 못하게 막는" 잠금 장치를 구현하는 데 정확히 필요한 기능이었다. 부가적으로, Slack 버튼에는 담을 수 있는 글자 수 제한이 있어서 워크플로의 긴 대기 토큰을 직접 넣을 수 없었다. 그래서 짧은 임시 키로 바꿔 이 저장소에 보관하고, 버튼 클릭 시 그 키로 원래 토큰을 조회하는 우회 구조를 썼다. ④ 생성형 AI(Bedrock) — 왜 AI를 끼워 넣었나, 그리고 왜 판단까지는 맡기지 않았나.. 문제: 복제 지연(초)이나 처리 대기량(byte) 같은 원시 수치는 새벽에 깨어난 당직자가 즉시 위험도를 판단하기 어렵다. 해결 — 그러나 역할은 제한적으로: 생성형 AI가 숫자를 사람이 읽을 수 있는 문장(위험도·예상 손실·권고 절차)으로 번역해주는 "설명 계층"으로만 쓰였다. 다만 "AI가 실수하면 시스템이 잘못 움직인다" 는 반대 리스크가 있으므로, 실제 상태 분기(승인 이후 무엇을 할지)는 AI가 아니라 정해진 규칙의 코드가 전담하도록 역할을 분리했다. 검수(사후 점검) 단계에서는 도구를 스스로 골라 쓰는 자율 루프 방식도 썼는데, 검수는 미리 정해둔 체크리스트만으로 다 커버할 수 없는 "이상 탐지형" 조사이기 때문이다. 상황에 따라 "서울을 다시 찔러볼지, DB 상태를 볼지"가 달라져야 하므로, 고정된 절차보다 필요에 따라 확인 방법을 스스로 고르는 방식이 이 요구에 맞았다. ⑤ Slack — 왜 새 UI를 안 만들었나.. 조직이 이미 상시 사용하는 협업 도구를 승인 인터페이스로 재사용했다. 새로운 대시보드나 앱을 따로 만들 필요 없이, 담당자가 휴대폰 알림만으로 즉시 반응(승인/거부)할 수 있다는 실용적인 이유다. API Gateway — 설계 의도가 아니라 제약으로 인한 대체 처음에는 Slack에서 호출할 공개 HTTPS 엔드포인트 하나만 있으면 충분하다고 보고 더 가벼운 방식으로 구현했다. 그런데 조직의 보안 정책이 인증 없는 공개 엔드포인트를 막고 있었고, 그 바람에 Slack에서 오는 요청이 막혀 승인 버튼이 동작하지 않는 문제가 있었다. API 게이트웨이로 앞단을 교체해 해결했는데, 뒤에 붙는 함수가 받는 요청 형식이 동일했던 덕분에 함수 코드는 한 줄도 바꾸지 않고 앞단 배선만 바꿔서 해결할 수 있었다. "공개 HTTPS 엔드포인트가 필요하다"는 원래 설계 의도는 그대로였고, 구현 수단만 조직 제약에 맞춰 바뀐 사례다. 5단계 — "실제 테스트 진행" 실제로 장애를 주입해보는 실험 코드 리뷰나 유닛 테스트로는 "리전 네트워크가 실제로 끊기면 무슨 일이 벌어지는지" 검증할 수 없다. Route53 헬스체크가 정말 반응하는지, 이상 탐지가 정말 작동하는지는 실제로 끊어봐야 안다. 그래서 서울 프라이빗 서브넷의 네트워크를 인위적으로 격리하는 장애 주입 실험(AWS FIS)을 진행했다. 이 실험에 앞서 관측 도구(CloudWatch Container Insights)부터 먼저 갖춰뒀다. 이유는 명확하다 — "지금 도쿄 클러스터가 버티고 있는지 눈으로 볼 수 없으면, 장애를 인위로 만드는 실험 자체가 의미가 없다." 관측 인프라가 먼저 갖춰져야 검증 도구를 쓸 자격이 생기는 순서다. 실제로 걸려 넘어졌던 함정 하나 Route 53은 특정 리전에 속하지 않는 글로벌 서비스다. 그런데 헬스체크를 어느 리전에서 만들었든 그 결과 지표는 항상 미국 동부(버지니아) 리전의 CloudWatch에만 게시된다 — AWS 플랫폼 자체의 고정된 동작이라 설계로 바꿀 수 있는 부분이 아니다. 이 함정에 나도 실제로 걸려 넘어진 적이 있다. 지난 테스트 때 알람을 서울 리전에 만들었다가 지표가 없다는 이유로 Slack 알림이 안 온 적이 있었고, 그 뒤로는 헬스체크 지표를 항상 미국 동부 리전 기준으로 고정해서 확인하도록 코드를 고쳤다. 이 경험을 겪으면서, 내가 설계를 "처음부터 완벽하게 해서 그대로 구현"한 게 아니라 "설계 → 구현 → 실전에서 실패 → 원인 파악 → 교훈을 반영해 재발 방지"를 반복하면서 다듬어왔다는 걸 새삼 느꼈다. 종합 — 하나의 흐름으로 지금까지의 다섯 단계를 하나로 이으면 다음과 같은 단일한 이야기가 된다. 서울이 통째로 죽을 수 있다 → 도쿄에 사본을 둔다(앱·DB·이미지·컨테이너 이미지 복제) → 트래픽은 자동으로 보낸다(DNS 전환, 가역적이라 안전) → 단, DB 승격은 사람이 결정(비가역·정합성 리스크) → 수동 결정이 느려지는 게 실측으로 드러남(공백 발생, RPO 목표 미달) → 판단을 돕는 자동화를 만들되 방아쇠는 사람이 당기게 → 이 자동화가 쓸 독립 저장소를 따로 둔다(순환 의존 방지) → 전체가 진짜 작동하는지 실제로 장애를 주입해본다 핵심 설계 철학 3가지 1. 되돌릴 수 있는지에 따라 자동화 수준을 달리한다 — 되돌리기 쉬운 것(DNS 전환)은 완전 자동, 되돌릴 수 없는 것(DB 승격)은 사람 승인을 반드시 거친다. 2. 판단을 돕는 것과 판단을 대신하는 것을 분리한다 — AI는 설명·조사·권고까지만 하고, 실제 상태 분기는 항상 정해진 규칙의 코드가 담당한다. 3. 설계는 한 번에 완성되지 않는다 — 리전 함정 사례처럼, 실전 테스트에서 발견된 실패를 다음 버전 설계에 그대로 반영하는 과정을 계속 반복했다.
![[DR] DR 아키텍처, 왜 이렇게 설계했나.. (DR 테스트 글 후속편)](/_next/image?url=https%3A%2F%2Fres.cloudinary.com%2Fdf4g2myjt%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1789748825%2Fblog%2Fpnys4mm6mqulxwwfluan.png&w=3840&q=75)
