NCP 계정 이전 리허설, 운영에 영향 없이 무엇을 검증했나
NCP 계정 이전 전 운영 데이터로 리허설하며 데이터 정합성, 기능, worker 격리를 검증하고 실제 전환에서 확인한 결과를 설명합니다.
목차
들어가며
데이터를 새 DB에 복원했다면 계정 이전을 시작해도 될까요? 복사 명령이 성공했더라도, 새 앱이 기존 메시지 서버로 요청을 보내거나 복원한 Redis에 시험 데이터가 남아 있다면 전환을 마쳤다고 보기 어렵습니다.
2026년 10월, 그룹웨어 WITIM의 NCP 운영 계정을 옮기면서 이런 문제를 미리 확인하기 위해 운영 데이터로 리허설을 진행했습니다. 점검 시간을 측정하고, 전환 당일 처음 겪을 문제를 미리 찾는 것이 목표였습니다.
2편에서는 PostgreSQL·Redis를 덤프·복원하고 잠깐 쓰기를 멈추는 방식을 선택했습니다. 당시 예상한 점검 시간은 15–20분이었습니다. 하지만 복원 시간만으로 전체 중단 시간을 예측할 수는 없었습니다. 새 앱을 띄우고, DB 연결과 채팅·파일 기능을 확인한 뒤 트래픽을 바꾸는 시간도 필요했기 때문입니다.
사용자가 서비스를 이용하지 못하는 동안 PostgreSQL 확장 오류나 잘못된 연결 주소를 처음 조사하게 되는 상황은 피하고 싶었습니다. 그래서 최종 전환과 같은 순서를 미리 실행하며 세 가지를 확인했습니다. 데이터와 기능이 정상인지, 전환에 얼마가 걸리는지, 당일 작업을 막을 문제가 있는지입니다.
다만 실제 데이터를 사용하는 만큼 리허설 자체가 운영에 영향을 주지 않도록 해야 했습니다. 복원된 DB에는 사용자 정보가 있고, 복사한 설정에는 푸시·메일·파일 저장소에 접근하는 비밀값도 포함돼 있었습니다. 이 글에서는 운영 DB를 보호하고 시험용 앱의 접근을 제한한 방법, 그리고 그 과정에서 발견한 문제와 실제 전환 결과를 설명하겠습니다.
1. 운영 DB는 한 번 읽고, 이후에는 복제본으로 시험했습니다
리허설을 시작하기 전에 자동 백업이 정상인지 확인했습니다. 그다음 운영 DB를 읽기 전용으로 한 번 덤프하고, 이후 복원과 테스트에는 그 파일을 사용했습니다. 이렇게 하면 시험을 반복할 때마다 운영 DB에 접속하거나 스키마를 수정할 필요가 없습니다.
덤프 파일은 먼저 로컬에서 확인하고 새 계정에 복원했습니다. 아래 그림은 그 과정에서 데이터가 이동하는 경로와 시험 환경의 접근 범위를 보여줍니다.
기존·새 계정 사이에는 VPC Peering을 연결하지 않았습니다. 관리형 DB는 NKS 노드 대역에서만 접근할 수 있었기 때문에, 덤프와 복원은 각각 클러스터 안의 임시 파드에서 실행했습니다. 파일은 kubectl cp로 옮겼고, 운영자 PC에서는 중간 파일과 검증 결과를 확인했습니다. 데이터 규모가 작아서 계정 간 상시 연결을 만드는 것보다 이 구조가 간단하다고 판단했습니다.
운영 DB에서 받은 파일은 먼저 로컬 Docker DB에 복원했습니다. 이 컨테이너는 네트워크를 차단해 다른 환경에 연결되지 않도록 했습니다. 로컬에서 확인한 뒤 같은 파일을 새 계정 DB에도 복원했습니다. 반복 시험 중 오류가 나더라도 운영 DB를 다시 건드리지 않도록 순서를 정한 것입니다.
잘못된 곳에 복원하는 실수를 막기 위한 검사도 스크립트에 넣었습니다. 원본 클러스터를 복원 대상으로 지정할 수 없게 하고, Kubernetes API 서버 주소와 PostgreSQL 서버 식별자, Redis run_id를 비교해 원본과 대상이 같은지 확인했습니다. 클러스터별 kubeconfig 파일도 분리했습니다. 새 환경의 설정이 잘못되더라도 운영 DB에 직접 닿을 네트워크 경로는 없었습니다.
2. 복사 명령이 성공해도 데이터가 맞는지는 따로 확인했습니다
PostgreSQL 접속 정보는 명령어에 직접 쓰지 않고 Kubernetes Secret으로 임시 파드에 전달했습니다. 원본 DB에 접속하는 세션에는 다음과 같이 읽기 전용 옵션을 적용했습니다.
PGOPTIONS='-c default_transaction_read_only=on'덤프는 pg_dump -Fc로 만들었습니다. 관리형 DB의 앱 계정에는 확장 설치 권한이 없었으므로, 필요한 확장은 대상 DB에 미리 준비하고 덤프 목차에서 해당 설치 항목을 제외했습니다. 기존 DB의 소유자·권한 정보도 새 계정에 그대로 적용하지 않았습니다.
복원에는 pg_restore -1 --exit-on-error를 사용했습니다. 한 트랜잭션에서 실행하고 오류가 발생하면 중단해, 복원 도중 실패했을 때 일부만 반영되는 상황을 막기 위해서였습니다.
그렇다면 명령이 오류 없이 끝났다는 것만으로 충분할까요? 복원 결과를 확인하기 위해 원본과 새 DB의 테이블 목록, 각 테이블의 정확한 행 수를 비교했습니다. 차이가 있으면 다음 단계로 넘어가지 않도록 했고, 리허설에서는 285개 테이블이 일치했습니다. 데이터가 50만 행 안팎이어서 통계상의 추정 행 수 대신 실제 count(*)로 확인하는 비용을 감당할 수 있었습니다.
물론 행 수가 같다고 모든 레코드 내용이 동일하다는 뜻은 아닙니다. 그래서 같은 덤프를 복원하고, 복원 중 오류가 있으면 즉시 중단하며, 테이블 수와 행 수를 대조하는 검증을 함께 사용했습니다. 어느 한 단계의 성공 메시지만으로 판단하지 않기 위해서였습니다.
Redis도 일부 세션만 골라 옮기지 않았습니다. 로그인 정보 외에 사용량 통계처럼 Redis에만 있는 데이터가 있었기 때문입니다. 같은 Redis 버전에서 DUMP·RESTORE를 사용하고, PTTL로 남은 만료 시간도 함께 옮겼습니다. 복사 후에는 키 수를 대조했습니다.
3. 새 앱의 요청이 운영 환경으로 나가지 않는지 확인했습니다
새 환경에서 기능을 시험하려면 앱에 접속해야 하지만, 아직 실제 사용자의 요청을 받을 단계는 아니었습니다. 그래서 운영자 맥북의 /etc/hosts만 수정했습니다. 그 PC에서는 새 로드밸런서로 접속하고, 실제 사용자들의 요청은 기존 NKS로 계속 향하도록 한 것입니다.
기능 확인에는 운영팀 계정과 개인 시험 계정만 사용했습니다. 두 계정만 있는 대화방에서 로그인·채팅·파일 업로드·관리자 기능을 확인했습니다.
여기서 주의할 점은 접속 주소를 바꿨다고 모든 연결이 새 환경을 향하는 것은 아니라는 점입니다. 파일 저장소처럼 환경변수에 복사된 외부 서비스는 여전히 실제 운영 자원을 가리킬 수 있었습니다. 따라서 테스트용 파일과 계정을 제한하고, 확인 후 파일을 정리했습니다. DNS를 바꾸지 않는 것만으로 외부 영향을 모두 막을 수는 없었습니다.
worker에는 별도의 격리가 필요했습니다. 웹 앱과 달리 실행 직후 Redis에 쌓인 예약 작업을 처리할 수 있기 때문입니다. 운영 데이터와 같은 Secret을 가진 worker가 푸시나 메일을 중복 발송하지 않도록 Cilium NetworkPolicy로 외부 통신을 차단했습니다. 새 DB·Redis 등 시험에 필요한 내부 통신만 허용했습니다.
정책을 적용한 뒤에도 설정만 보고 격리가 됐다고 판단하지 않았습니다. worker를 기동하기 전에 파드에서 실제 외부 통신이 차단되는지 확인했습니다. 그다음 worker를 실행하고, 내부 DB와 Redis를 정상적으로 사용하는지 살펴봤습니다.
리허설에서는 worker를 19분 동안 실행해 13개 큐의 약 650건을 처리했고, DB·Redis 오류는 없었습니다. 다만 외부 호출을 차단한 시험이므로 이 결과가 푸시나 메일 발송 성공까지 보여주지는 않습니다. 외부 발송은 실제 운영 전환 후 확인해야 할 항목으로 구분했습니다.
4. 리허설에서 실제로 발견한 문제
리허설에서는 예상하지 못한 문제 세 가지를 발견했습니다. 앱이 실행 중인지, 복사 명령이 성공했는지만 확인했다면 지나치기 쉬운 문제들이었습니다. 각 문제가 어떤 시험에서 드러났고 언제, 어떻게 대응했는지 차례로 살펴보겠습니다.
4-1. 새 웹에서 보낸 메시지가 기존 Centrifugo로 전달됐습니다
새 계정의 웹에 접속해 채팅을 시험했는데, 메시지가 새 클러스터가 아니라 기존 운영 Centrifugo로 발행되고 있었습니다. 복사해 둔 CENTRIFUGO_API_URL이 기존 서비스의 공개 주소를 가리키고 있었기 때문입니다.
아래 그림은 당시 메시지가 향하던 곳과 리허설 중 수정한 연결 경로를 비교한 것입니다.
새 웹은 정상으로 뜨고 로그인도 가능했습니다. 하지만 실시간 메시지는 웹 서버에서 별도의 Centrifugo API로 발행됩니다. 브라우저가 새 웹에 접속하더라도, 웹 서버가 메시지를 어디로 보내는지는 설정값에 따라 달라집니다. 이 경로를 따로 확인하면서 기존 주소가 그대로 복사됐다는 사실을 발견했습니다.
이대로 전환했다면 웹과 DB는 새 계정을 사용하면서 메시지는 기존 계정으로 발행하는 구성이 됩니다. 새 클러스터 내부의 실시간 연결과 실제 메시지 경로가 일치하지 않아, 전환 당일에 채팅 지연이나 전달 오류를 조사해야 했을 것입니다.
리허설 중 설정을 새 클러스터의 내부 Centrifugo 주소로 바꿨습니다. 실제 전환 당일에도 메시지가 즉시 표시되는지 확인했습니다. 이 경험을 바탕으로 리허설 체크리스트에 서비스의 실행 상태뿐 아니라 내부 의존 서비스의 실제 접속 대상도 확인하는 항목을 추가했습니다.
4-2. PostgreSQL 확장 스키마 때문에 첫 복원이 실패했습니다
PostgreSQL은 첫 시도에서 복원이 실패했습니다. vector, pg_stat_statements 확장이 예상한 스키마가 아니라 NCP 관리형 DB의 cdb_admin 스키마에 설치돼 있었습니다. 대상 DB에 확장을 미리 준비했더라도, 복원 과정에서 이 스키마 차이를 처리하지 못하면 오류가 발생했습니다.
초기 스크립트는 확장 설치 문장을 제외하면 나머지 복원이 정상적으로 진행될 것이라고 가정했습니다. 하지만 관리형 PostgreSQL에서는 확장을 설치할 수 있는 계정과 스키마가 일반적인 환경과 다를 수 있습니다. 같은 메이저 버전의 PostgreSQL을 준비한 것만으로 내부 구성까지 같다고 볼 수는 없었습니다.
기존 복원 runner를 수정해 덤프 파일에서 확장의 스키마 정보를 읽고 그에 맞게 처리하도록 했습니다. 수정 후 다시 복원하고, 285개 테이블의 행 수가 일치하는지 확인했습니다. 오류가 사라진 것에 그치지 않고 원래 정한 검증 기준을 다시 통과하는지 확인한 것입니다.
운영 전환 중에 이 오류를 처음 만났다면 쓰기를 멈춘 상태에서 원인을 조사하고 복원을 재시도해야 했을 것입니다. 이전 전용 스크립트를 만들 때는 명령의 문법과 옵션뿐 아니라, 복원 대상의 관리형 DB 특성도 실제 덤프로 시험해야 한다는 점을 확인했습니다.
4-3. Redis를 덮어쓴 뒤에도 이전 리허설의 키 699개가 남았습니다
Redis는 복사 명령이 성공했는데도 원본과 대상의 키 수가 맞지 않았습니다. 확인해 보니 이전 리허설에서 생긴 키 699개가 대상에 남아 있었습니다.
사용한 redis-import --replace는 같은 이름의 키를 덮어씁니다. 하지만 대상에만 있는 키를 삭제하지는 않습니다. replace가 충돌하는 키를 교체한다는 의미를 대상 전체를 원본과 동일하게 만든다는 의미로 이해했던 것이 원인이었습니다. 리허설을 반복하면 이전 시험의 키가 대상에 계속 남을 수 있었습니다.
Redis에는 캐시뿐 아니라 로그인 세션, 모바일 세션 포인터, 사용량 통계처럼 복원 시점의 상태를 정확히 맞춰야 하는 데이터도 있었습니다. 복사 후 키 수를 대조하는 검증이 있었기 때문에 차이를 발견할 수 있었습니다. 명령의 성공 여부만 확인했다면 놓치기 쉬운 부분이었습니다.
이 문제는 리허설에서 발견했지만, 복사 도구를 완전히 고친 시점은 운영 전환 이후였습니다. 전환 당일에는 잔여 키를 수동으로 대조·정리해 최종적으로 9,587개 키가 일치하는 것을 확인했습니다. 이후 runner를 ‘기존 키를 덮어쓰기’에서 ‘대상을 비우고 복원하기’ 방식으로 수정했습니다.
즉, 리허설에서 문제를 찾은 뒤 전환 당일에는 수동 검증으로 대응했고, 도구의 영구 수정은 사후에 진행했습니다. 문제 발견과 모든 수정이 리허설 안에서 끝난 것은 아닙니다. 전환 당일의 수동 검증과 사후 도구 개선을 구분해서 볼 필요가 있습니다.
세 문제의 원인은 각각 기존 서비스 주소가 남은 환경변수, 관리형 PostgreSQL의 확장 스키마, Redis의 replace 동작에 대한 가정이었습니다. 모두 실제 결과를 대조하지 않았다면 놓칠 수 있었습니다.
리허설 이후에는 데이터 복사를 실행할 수 있는지에 더해, 새 웹이 새 실시간 서버를 사용하는지, 관리형 DB의 복원과 행 수 대조가 끝까지 통과하는지, Redis에 원본에 없는 키가 남아 있지는 않은지 확인하도록 전환 당일의 검증 항목을 보강했습니다.
5. 10월 23일 실제 전환 결과
리허설을 마친 뒤 새 앱은 대기 상태로 되돌렸습니다. 실제 전환에서는 10월 22일 23시 59분에 기존 서비스의 쓰기를 멈췄습니다. 23일 00시 1–5분에 PostgreSQL과 Redis를 최종 복사하고, 00시 6분에 새 앱을 기동한 뒤 00시 12분에 Cloudflare 진입점을 전환했습니다.
| 검증 항목 | 결과 |
|---|---|
| PostgreSQL | 285개 테이블, 495,192행 일치 |
| Redis | 9,587개 키 일치 |
| 사용자 영향 | 약 13분 |
| 전환 후 1시간 웹 요청 | 5,518건, 5xx 0건 |
| 같은 기간 worker 작업 | 1,926건 성공, 재시작 0회 |
웹·데스크톱·모바일의 로그인 유지와 실시간 채팅·파일·푸시도 확인했습니다. 사전 리허설에서 측정한 데이터 이동 시간은 약 2분이었지만, 실제 전환에는 앱 중지·기동과 기능 확인도 포함됐습니다. 따라서 데이터 복사 시간과 전체 사용자 영향 시간은 구분해서 봐야 합니다.
이번 전환에서는 13분이라는 결과와 함께, 그 결과를 확인한 절차도 중요했습니다. 원본과 대상의 데이터를 비교할 기준이 있었고, 문제가 발생하면 어떤 단계에서 멈출지도 미리 정해 두었습니다. 전환을 마친 뒤에는 별도로 배포 경로를 검증하고 모니터링을 점검했습니다.
마무리하며
리허설에서는 실제 데이터를 복원하는 과정과 새 앱이 연결되는 곳을 함께 확인했습니다. 잘못된 클러스터로 복원하지 않도록 막고, 복제한 앱이 운영 환경을 건드리지 않도록 격리한 뒤, 최종 전환 전에 예상하지 못한 연결과 데이터 차이를 찾았습니다.
그 과정에서 PostgreSQL 확장 스키마, Centrifugo 주소, Redis 잔여 키 문제를 발견했습니다. 점검 시간에 처음 만났다면 전환이 길어지거나 데이터를 다시 대조해야 했을 문제들입니다. 2편에서 선택한 이전 방식을 실제로 적용하려면, 같은 순서로 반복할 수 있는 검증 절차가 함께 필요했습니다.
시리즈: 1편 · Terraform·NKS로 운영 환경 이전 · 2편 · 이전 방식의 의사결정 · 3편 · 운영 데이터로 리허설과 검증
댓글