무중단 마이그레이션의 본질은 이동이 아니라 되돌릴 수 있는 다리 놓기입니다
레거시 서비스나 데이터베이스를 새 스택으로 옮겨야 하는 백엔드 엔지니어, 그리고 그 일정과 위험을 책임지는 소규모 팀의 기술 리드라면 이 글에서 하나의 답을 가져갈 수 있습니다. 마이그레이션이 무너지는 지점은 거의 항상 새 기술의 완성도가 아니라, 어느 순간에든 돌아갈 자리를 남겨두지 못했다는 사실입니다. 무중단 이전을 성공시키는 힘은 속도나 새 아키텍처의 우아함이 아니라, 매 단계마다 실패해도 되돌릴 수 있는 상태를 끝까지 유지하는 설계에서 나옵니다.
마이그레이션을 흔히 이삿짐 나르는 일처럼 상상합니다. 짐을 트럭에 싣고 새집에 부리면 끝나는 일처럼 말이죠. 실제로는 옛 시스템과 새 시스템이 한동안 같은 사실을 놓고 서로 다른 경로로 답하다가, 어느 시점에 하나가 조용히 권한을 넘겨받는 훨씬 길고 위태로운 과정입니다. 그 과정을 지탱하는 것은 새 시스템이 얼마나 나은가가 아니라, 매 단계 뒤에 되돌아갈 다리 하나가 항상 놓여 있다는 사실입니다. 다리가 없는 순간이 딱 한 번이라도 있다면, 그 순간에 걸리는 사고가 프로젝트 전체의 성패를 가릅니다.
이 글은 그 다리를 어떻게 놓고 어디서 끊어지기 쉬운지, 그리고 걷어내는 마지막 순서까지 하나의 흐름으로 따라갑니다.
글의 핵심 개념을 형상화했습니다.
“이동”이라는 단어가 마이그레이션을 오해하게 만드는 이유
“이동”이라는 단어는 완료 시점을 상상하게 만듭니다. 다 옮기고 나면 프로젝트가 끝난다는 그림이죠. 그런데 실제 시간표를 펼쳐 보면 두 시스템이 공존하는 기간이 전체 일정의 대부분을 차지합니다. 트래픽 전환은 하루 만에 끝나는 사건이지만, 그 하루를 안전하게 만드는 준비와 이후의 관측은 몇 주에서 몇 달까지 이어집니다. 프로젝트를 “이동”으로 정의하면 이 긴 공존 기간이 낭비처럼 보이고, 팀은 그 기간을 서둘러 줄이려는 유혹에 빠집니다.
되돌릴 수 있음을 기준으로 삼으면 이야기가 달라집니다. 성공 조건이 새 시스템이 옛 시스템보다 나은가가 아니라, 무언가 잘못됐을 때 몇 초 안에 원래 자리로 돌아갈 수 있는가로 바뀝니다. 이 관점 하나로 어떤 작업을 먼저 하고 어떤 작업을 미룰지, 어느 단계에 시간을 얼마나 쓸지가 정리됩니다. 되돌리기 어려운 작업일수록 준비가 두꺼워져야 하고, 되돌리기 쉬운 작업은 빠르게 반복해도 괜찮습니다.
다리는 건너가는 용도인 동시에 되돌아오는 용도입니다. 마이그레이션의 각 단계는 다리 위의 한 지점이고, 그 지점에서 계속 앞으로 갈지 뒤로 물러날지를 언제든 선택할 수 있어야 정상입니다. 뒤에서 살펴볼 다섯 개의 실무 단계는 각각 이 다리의 서로 다른 구간을 놓는 작업입니다.
자를 수 있는 자리에서만 시작할 수 있습니다
“결제 시스템을 새 스택으로 옮기자”는 문장에는 옮길 대상이 실제로 정의되어 있지 않습니다. 결제 시스템이라는 이름 아래 API 서버, 정산 배치, 관리자 화면, 여러 데이터베이스 테이블 묶음, 그리고 누구도 정확히 기억하지 못하는 크론 잡 여러 개가 뒤섞여 있기 때문입니다. 이 덩어리를 한꺼번에 들어 옮기려 하면 무중단은 애초에 불가능한 목표가 됩니다. 들 수 있는 크기로 쪼개는 작업이 모든 이전 프로젝트의 진짜 첫 단계입니다.
문제는 쪼개는 기준을 잘못 고르는 경우가 흔하다는 것입니다. 조직도나 기능 명세를 따라 자르면 그럴듯해 보이지만, 실제 코드가 그 경계를 지키고 있지 않으면 존재하지 않는 경계입니다. 쿠폰 기능을 먼저 옮기자는 계획은 합리적으로 들리지만, 쿠폰 로직이 주문 테이블을 직접 갱신하고 있다면 쿠폰과 주문은 이미 하나의 단위입니다. 자를 수 있는 자리는 기능 명세가 아니라 코드 안에 실제로 존재하는 지점뿐입니다.
그 지점은 대개 네 가지 형태로 나타납니다. 호출자를 고치지 않고도 구현을 바꿔 끼울 수 있는 네트워크 경계, 즉 HTTP 엔드포인트나 gRPC 서비스, 메시지 큐의 토픽입니다. 인터페이스 하나로 감싸인 모듈이나 리포지터리 계층도 마찬가지 역할을 합니다. 다른 컴포넌트가 조인해서 들여다보지 않는 테이블 묶음, 그리고 입력과 출력이 파일이나 테이블로 완전히 끊긴 배치 잡도 자를 수 있는 자리입니다. 이 네 가지 중 어디에도 해당하지 않는 대상은 아직 이전 단위가 아닙니다.
이 네 자리 중 어느 곳에도 걸리지 않는다면, 옮기기 전에 그 지점을 먼저 만들어야 합니다. 인터페이스를 하나 뽑아내고 기존 구현을 그 뒤로 밀어 넣는 리팩터링은 이전 작업이 아니라 이전을 위한 준비이며, 건너뛰면 되돌아갈 자리 자체가 없어 나중에 롤백이 물리적으로 불가능해집니다. 경계를 그을 때 지켜야 하는 규칙도 하나 있습니다. 읽기와 쓰기를 같은 표로 뭉뚱그리지 말아야 합니다. 마이그레이션의 실제 난이도는 거의 전적으로 쓰기 쪽에서 결정되기 때문입니다.
파사드는 전환 장치가 아니라 관측 장치입니다
옛 시스템을 새 시스템으로 바꾸는 방법은 크게 두 가지로 나뉩니다. 새것을 완전히 만든 다음 어느 주말에 한꺼번에 갈아 끼우는 방법, 아니면 앞에 얇은 층을 하나 세우고 그 뒤에서 조금씩 바꿔 끼우는 방법입니다. 앞의 방식은 계획이 단순한 대신 실패했을 때 돌아갈 곳이 없습니다. 뒤의 방식이 이른바 스트랭글러 방식이고, 새 구현이 옛 구현을 조금씩 감싸며 대체해 나갑니다.
이 방식의 핵심은 새 시스템을 얼마나 훌륭하게 만드느냐가 아니라, 호출자가 응답한 쪽이 옛 시스템인지 새 시스템인지 신경 쓰지 않아도 되게 만드는 데 있습니다. 그래서 첫 작업은 새 코드를 짜는 일이 아니라, 호출자와 옛 구현 사이에 층을 하나 끼워 넣는 일입니다. 이 층, 즉 파사드는 처음에는 아무 일도 하지 않습니다. 받은 요청을 그대로 옛 구현에 넘기고 응답을 그대로 돌려줄 뿐입니다.
이 단계가 지루해서 건너뛰고 싶은 유혹이 생기지만, 여기서 세 가지를 확보하게 됩니다. 첫째, 호출자 목록이 확정됩니다. 파사드를 거치지 않는 호출자가 남아 있다면 그 자체가 사고의 씨앗입니다. 둘째, 요청과 응답의 실제 모양이 로그로 드러납니다. 명세와 실제 데이터가 다른 경우는 놀랄 만큼 흔하고, 파사드는 그 차이를 조용히 알려줍니다. 셋째, 나중에 라우팅 규칙을 넣을 자리가 이미 마련됩니다.
파사드를 놓는 위치는 시스템의 형태에 따라 달라집니다. HTTP 서비스라면 리버스 프록시나 게이트웨이의 경로 규칙이, 라이브러리 호출이라면 인터페이스 하나와 위임 구현이, 메시지 기반 시스템이라면 중계하는 소비자가, 데이터베이스 접근이라면 리포지터리 계층이나 뷰가 그 자리를 맡습니다. 다만 파사드를 세우면서 동시에 인터페이스를 개선하려는 시도는 피해야 합니다. 필드 이름이 마음에 들지 않아도 이 단계에서는 그대로 통과시키는 것이 원칙입니다. 개선과 이전을 같은 커밋에 담으면, 문제가 생겼을 때 원인이 어느 쪽인지 구분할 수 없게 됩니다.
되돌릴 수 있음을 만드는 재료는 순서와 멱등성입니다
요청 경로를 옮기는 일은 설정 값 하나만 바꾸면 되돌릴 수 있습니다. 데이터는 전혀 다른 문제입니다. 새 저장소에만 기록된 주문 한 건은 라우팅을 되돌린다고 해서 옛 저장소로 돌아오지 않기 때문에, 무중단 이전의 진짜 난이도는 대부분 데이터를 옮기는 이 단계에 몰려 있습니다. 목표는 명확합니다. 전환하든 되돌리든 어느 시점에든 두 저장소가 같은 사실을 담고 있어야 하고, 같다는 사실을 감이 아니라 숫자로 증명할 수 있어야 합니다.
이중 쓰기는 한 번의 쓰기 요청을 두 저장소에 동시에 반영하는 방식입니다. 그런데 이중 쓰기만 켜 놓으면 새 저장소에는 오늘 이후의 데이터만 쌓이고 어제까지의 데이터는 비어 있어, 읽기를 새 저장소로 돌리면 과거를 조회하는 요청이 전부 빈 결과를 내게 됩니다. 그래서 이중 쓰기와 백필은 항상 짝으로 움직여야 합니다. 이중 쓰기는 지금 이후에 발생하는 변경을 양쪽에 반영하는 일이고, 백필은 그 시작 시점 이전의 데이터를 새 저장소로 복사하는 일입니다.
순서가 결과를 결정합니다. 이중 쓰기를 먼저 켜고 그다음 백필을 돌려야 합니다. 반대로 하면 백필이 도는 동안 들어온 변경이 새 저장소에 반영되지 않는 구멍이 생깁니다. 이중 쓰기를 먼저 켜면 백필은 과거 구간만 채우면 되고, 겹치는 구간은 같은 값을 두 번 쓰는 셈이라 결과가 달라지지 않습니다. 이 겹침이 안전하려면 백필 잡이 멱등해야 합니다. 몇 번을 다시 돌려도 결과가 같아야 한다는 뜻이고, 삽입은 있으면 갱신하고 없으면 삽입하는 방식이어야 하며, 갱신 기준은 실행 시각이 아니라 원본이 실제로 바뀐 시각이어야 합니다. 이 조건 하나가 뒤늦게 도착한 백필이 최신 값을 과거 값으로 덮어쓰는 사고를 막아 줍니다.
백필은 한 번에 전부 돌리지 않고 나눠서 돌리는 편이 안전합니다. 기본 키 범위나 생성 시각 구간으로 잘라 실행하면, 중간에 문제가 생겨도 어디까지 진행됐는지 알 수 있고 그 지점부터 다시 시작할 수 있습니다. 백필이 끝났다는 판단도 잡의 종료 코드가 아니라, 두 저장소의 레코드 수와 체크섬이 실제로 일치한다는 확인에서 나와야 합니다. 검증 없는 백필은 완료가 아니라 완료로 추정되는 상태일 뿐입니다.
전진 절차와 롤백 절차를 같은 무게로 씁니다
컷오버는 마이그레이션에서 가장 짧은 순간이면서 가장 많은 준비를 요구하는 순간입니다. 앞의 단계들을 제대로 밟아 왔다면 컷오버 자체는 설정 값 하나를 바꾸는 일에 불과합니다. 실행하는 그 순간이 지루하게 느껴진다면 준비가 잘된 것이고, 반대로 긴장되고 극적으로 느껴진다면 어딘가를 건너뛰었다는 신호입니다.
대부분의 팀이 컷오버 계획서를 쓸 때 전진 절차만 상세하게 적습니다. 몇 시에 무엇을 하고, 누가 확인하고, 다음 단계로 언제 넘어가는지까지 촘촘하게 채워 넣고는, 마지막 줄에 문제가 생기면 롤백한다는 한 줄만 덧붙입니다. 그 한 줄은 새벽 세 시에 실제로 문제가 터졌을 때 아무런 도움이 되지 않습니다. 롤백 절차도 전진 절차와 같은 수준으로, 어떤 명령을 어떤 순서로 누가 실행하는지까지 구체적으로 적어야 합니다.
특히 자주 빠지는 항목이 하나 있습니다. 롤백으로 되돌린 뒤, 그사이 새 경로가 이미 만들어 낸 데이터는 어떻게 처리할 것인가라는 질문입니다. 이중 쓰기를 아직 켜 둔 상태라면 그 데이터는 양쪽 저장소에 모두 남아 있으니 문제가 없습니다. 하지만 이중 쓰기를 이미 껐다면 그 구간의 데이터는 새 저장소에만 존재합니다. 그래서 이중 쓰기를 끄는 시점은 컷오버가 끝난 직후가 아니라 충분한 관측 기간이 지난 뒤여야 합니다.
계획서에 담아야 하는 항목은 이렇습니다. 단계별 확인 항목이 있는 전진 절차, 같은 형식의 역순 롤백 절차, 어떤 숫자가 어떻게 움직이면 멈추는지 정한 중단 기준, 관측 기간, 조작하는 사람과 지표를 보는 사람과 판단하는 사람을 나눈 역할 구성, 그리고 판단자가 자리에 없을 때의 연락 체계입니다. 한 사람이 조작과 판단을 동시에 맡으면 눈앞의 숫자에 휘둘려 원칙을 벗어날 위험이 커지므로 역할을 나눕니다.
“끝났다”의 정의를 다시 세웁니다: 꺼야 끝난 것입니다
마이그레이션 프로젝트의 대다수는 여기서 멈춥니다. 트래픽은 새 시스템으로 넘어갔고 지표도 안정적이고, 팀은 이미 다음 일로 관심을 옮겼습니다. 옛 시스템은 혹시 모른다는 이유로 켜 둔 채 남고, 몇 년 뒤에는 누구도 그 서버가 정확히 무엇을 하는지 기억하지 못하는 상태로 청구서에 계속 이름이 찍힙니다.
꺼지지 않은 레거시는 이전이 끝난 상태가 아니라 두 시스템을 동시에 운영하는 상태입니다. 비용도 두 배, 보안 패치 대상도 두 배, 온콜 담당자가 알아야 할 지식도 두 배로 쌓입니다. 게다가 옛 시스템이 살아 있는 한 누군가는 결국 그쪽에 새 기능을 붙이게 되고, 그 순간 마이그레이션은 사실상 원점으로 돌아갑니다.
끄기 전에 던져야 하는 질문은 하나뿐입니다. 아직 이걸 쓰는 사람이 있는가입니다. 코드 검색만으로는 답이 나오지 않습니다. 저장소 밖에서 호출하는 경로가 항상 남아 있기 때문입니다. 운영자의 로컬 스크립트, 사내 위키의 컬 명령, 분석팀의 예약 쿼리, 파트너사 시스템, 몇 년 전 설정해 두고 잊어버린 모니터링 프로브 같은 것들입니다. 확인할 자리는 접속 로그의 호출자 아이피, 데이터베이스 접속 통계, 방화벽과 보안 그룹 로그, 아직 쓰이는 토큰을 보여주는 인증 서버 로그, 그리고 배치 스케줄러입니다.
관측 기간을 90일로 잡는 이유는 월 단위와 분기 단위로만 도는 작업들 때문입니다. 30일만 확인하고 껐다가 분기 마감 배치가 죽는 사고는 드물지 않습니다. 회계나 정산이 걸려 있다면 연간 작업이 한 번은 지나가야 하므로 400일 정도가 안전합니다. 호출자를 찾았다면 각각에게 담당자를 지정하고, 담당자가 직접 확인하고 동의해야 그 호출을 끊을 수 있습니다.
다리를 걷어내는 순서가 곧 마이그레이션의 완성입니다
옛 시스템을 끄는 순간은 지금까지 놓아 온 다리 전체를 걷어내는 순간이기도 합니다. 그 순간부터는 더 이상 되돌릴 수 없다는 사실이, 왜 종료 작업이 새 시스템을 만드는 작업과 똑같은 신중함을 요구하는지를 설명합니다. 되돌릴 수 있음이란 영원한 안전망이 아니라, 필요한 동안만 붙여 두고 확신이 생겼을 때 스스로 걷어내는 임시 장치입니다. 프로젝트 전체를 한 흐름으로 보면, 각 단계가 얼마나 되돌리기 쉬운지가 처음부터 끝까지 점점 좁아지는 모습이 드러납니다.
| 단계 | 되돌리는 방법 | 되돌리는 데 걸리는 시간 |
|---|---|---|
| 파사드 도입 | 파사드 설정을 원래 위임 구조로 되돌린다 | 즉시 |
| 이중 쓰기 시작 | 이중 쓰기 플래그를 끈다 | 즉시 |
| 백필 실행 | 재실행하거나 중단한다, 멱등하므로 안전하다 | 즉시 |
| 컷오버, 읽기 전환 | 라우팅 설정 값 하나를 되돌린다 | 즉시 |
| 이중 쓰기 종료 | 되돌릴 수 없다, 관측 기간이 끝난 뒤에만 수행한다 | 불가 |
| 옛 시스템 종료 | 되돌릴 수 없다 | 불가 |
이 표가 보여주는 흐름이 이 글 전체의 결론입니다. 이음새를 찾고 파사드를 세우고 이중 쓰기를 켜는 초반의 결정은 언제든 즉시 되돌릴 수 있는 값싼 결정이라, 완벽을 추구하기보다 빠르게 시도하고 관찰하는 태도가 맞습니다. 반대로 이중 쓰기를 끄고 옛 시스템을 종료하는 뒤쪽의 결정은 되돌릴 수 없는 비싼 결정이라, 충분한 관측 기간과 실제 호출자 확인이라는 느린 절차가 정당화됩니다.
마이그레이션을 어렵게 만드는 것은 기술 스택의 차이가 아니라, 이 두 종류의 결정을 같은 속도로 처리하려는 시도입니다. 값싼 결정을 지나치게 신중하게 다루면 프로젝트가 끝없이 지연되고, 비싼 결정을 서둘러 처리하면 되돌릴 수 없는 사고가 납니다. 지금 진행 중인 마이그레이션이 있다면, 각 단계를 이 표의 어느 행에 놓을 수 있는지 점검하는 것이 다음 할 일을 정하는 가장 빠른 방법입니다.
더 구체적인 절차와 체크리스트가 필요하다면, 이 글이 다룬 다섯 단계를 실전 순서와 세부 항목까지 풀어놓은 전자책 《멈추지 않고 옮기기》를 참고해 보세요.