Big Project · AI's EYE 40
브랜치: 코드 이름표를 넘어선 '릴리즈 레일'
develop과 main branch만 나누면 개발·운영 환경 분리가 끝난 것처럼 보인다. 왜 각 branch가 다른 runner, network와 배포 검증으로 이어져야 하는지 궁금하다.
근거 · 프로젝트 문서 · 코드 · 테스트
1장: 초점 흐리기: 브랜치, 그 이상의 약속
솔라의 손가락이 스크린 위 한 문장에 머물러 있었다. 이미 몇 번이나 곱씹은 문장이었다.
develop push는 Tailscale을 거쳐 미니PC에, main push는 OIDC·IAP를 거쳐 GCP 운영 서버에 배포된다.
단어 하나하나 뜯어보면 모르는 말은 없었다. develop과 main은 매일같이 쓰는 브랜치 이름이었고, push는 코드를 원격 저장소로 밀어 올리는 익숙한 명령어였다. 하지만 문장이 합쳐지자, 솔라가 알고 있던 세계의 물리 법칙이 어긋나는 듯한 위화감이 들었다.
“언니, 나 이거 아무리 생각해도 이상해.”
솔라가 화면을 가리키자, 조용히 자기 코드를 보던 루나가 시선을 돌렸다.
“뭐가?”
“이거. develop이랑 main 브랜치가 있잖아. 코드를 고치고 테스트한 다음에 develop에 합치고, 다 괜찮으면 main으로 보내서 배포하는 거. 이건 알겠어. 그런데 push를 한다고 해서 어떻게 가는 길이 달라져? 내 생각에 git push는 그냥 내가 만든 코드 뭉치를 깃허브 같은 원격 저장소에 올리는 거잖아. 그럼 develop으로 push하든 main으로 push하든, 일단 깃허브에 코드가 업데이트되는 똑같은 일이 일어나야 하는 거 아니야?”
솔라의 목소리에는 진심 어린 의문이 담겨 있었다. 그녀에게 브랜치는 코드의 특정 상태를 가리키는 편리한 이름표, 그 이상도 이하도 아니었다. ver-1.0, ver-1.2-hotfix 처럼 말이다. 이름표가 다르다고 해서 그 이름표가 붙은 상자를 운반하는 방식 자체가 바뀔 이유는 없어 보였다.
루나는 설명하는 대신, 솔라의 생각에 발을 맞춰주기로 했다.
“좋아, 솔라. 그럼 네가 생각하는 이상적인 그림을 한번 그려보자. 네가 feature-login이라는 브랜치에서 작업을 다 끝내고 develop 브랜치에 코드를 push했어. 그럼 그 다음에 무슨 일이 일어나야 할까?”
“음… 깃허브 저장소의 develop 브랜치가 최신 코드로 업데이트 되겠지. 그럼 개발 서버에서 그 변경 사항을 git pull 같은 명령어로 받아와서 다시 실행시켜주면 되는 거 아냐?”
“좋아. 그럼 그 코드가 안정화돼서 실제 사용자들이 쓸 main 브랜치에 push하면?”
“똑같지. 이번엔 운영 서버에서 git pull해서 새로고침 해주면 되지. 물론 좀 더 조심해야겠지만… 근본적으로는 같은 원리 아닐까?”
솔라는 자신 있게 대답했다. 그녀의 세계에서 배포는 코드의 동기화 문제였다. 가장 최신 버전의 코드가 올바른 장소에서 실행되기만 하면 되는 것이었다.
루나는 말없이 솔라가 가리켰던 문장으로 다시 고개를 돌렸다.
“그런데 저 문장은 아니라고 하네.”
루나의 차분한 한마디에 솔라의 확신에 금이 가기 시작했다.
“저기엔 develop으로 가는 길엔 Tailscale이라는 터널이, main으로 가는 길엔 OIDC와 IAP라는 보안 검색대가 있다고 하잖아. 네 설명 속 ‘똑같은 원리’에는 이 다른 길, 다른 장치들이 들어갈 자리가 없는데.”
솔라는 입을 다물었다. 그랬다. 자신의 설명은 너무나 단순했다. 마치 서울에서 부산까지 걸어가나 KTX를 타나, ‘이동한다’는 점에서는 같다고 말하는 것과 비슷했다. 과정의 복잡성과 목적의 차이를 완전히 무시한 설명이었다. Tailscale, OIDC, IAP… 지금 당장 저것들이 무엇인지 아는 게 중요한 게 아니었다. 중요한 것은, develop과 main이라는 서로 다른 이름표가 완전히 다른 운송 시스템을 불러냈다는 사실 그 자체였다.
생각이 거기에 미치자, 안개가 걷히는 기분이었다.
“아…”
탄식과 함께 깨달음이 찾아왔다.
“그럼 git push origin develop 이라는 명령은… 그냥 ‘깃허브에 코드 올려줘’라는 뜻이 아니었구나. ‘develop이라고 이름 붙은 경로로 출발시켜줘’ 라는 신호였던 거네.”
솔라의 눈빛이 달라졌다. 더 이상 push는 단순한 업로드 명령이 아니었다. 그것은 거대한 자동화 시스템의 기차표를 발권하는 행위였다. 그리고 그 기차표에는 목적지뿐만 아니라, 어떤 철로를 따라 어떤 보안 검문을 거쳐 갈지가 모두 담겨 있었다. develop행 티켓과 main행 티켓은 목적지도, 거쳐 가는 길도 전혀 다른 별개의 것이었다.
“맞아. git push는 같아 보여도, develop이냐 main이냐에 따라 전혀 다른 기차에 코드를 실어 보내는 거야.”
솔라는 고개를 끄덕이며 혼잣말처럼 중얼거렸다.
“그러니까… 브랜치는 그냥 코드 뭉치에 붙이는 이름표가 아니었어. 그 코드를 실어 나를 전체 배포 과정, 그 ‘릴리즈 레일’을 선택하는 출발 신호였던 거야.”
이제야 솔라는 처음 봤던 그 문장의 무게를 이해할 수 있었다. 그것은 단순한 사실의 나열이 아니라, 두 개의 완전히 다른 세계로 이어지는 철로 설계도에 대한 요약본이었다. 브랜치는 그 철로를 선택하는 분기점의 스위치였다.
혼란은 명확한 호기심으로 바뀌었다. 이제 분기점이 있다는 것은 알았다. 그렇다면 그 첫 번째 길은 어떻게 생겼을까?
“언니, 그럼 알겠어. 궁금한 게 생겼어. 그 develop 브랜치에 연결된 릴리즈 레일, 그 ‘미니PC행 열차’는 대체 어떻게 생긴 거야? 왜 하필 그런 길을 써야만 했을까?“
2장: 미니PC 행 열차: develop 레일의 구성
솔라의 질문이 남긴 공기 중의 미세한 진동이 가라앉자, 루나는 키보드에 손을 올렸다. 대답 대신, 그녀는 화면에 떠 있던 복잡한 코드 창을 닫고 새하얀 디지털 캔버스를 열었다. 그리곤 간결하게 두 개의 사각형을 그렸다. 왼쪽 사각형 안에는 ‘GitHub Actions Runner (인터넷 어딘가)’라고 적었고, 오른쪽에는 ‘미니PC (우리 집 구석)’이라고 썼다. 둘 사이에는 아무런 연결선 없이, 텅 빈 공간만이 놓여 있었다.
루나는 마우스 커서로 두 사각형 사이의 공백을 빙글빙글 돌리며 아무 말 없이 솔라를 바라보았다. 이전 장에서 얻은 깨달음으로 달아올랐던 솔라의 머리가 다시 차갑게 식는 느낌이었다. 브랜치가 ‘출발 신호’라는 것을 이해한 것은 좋았지만, 신호 다음의 실제 경로를 그리려 하니 막막했다. 언니가 그려놓은 저 그림이 바로 그 막막함의 표현이었다. 인터넷 어딘가에 떠 있는 임시 컴퓨터와, 우리 집 구석에 있는 작은 서버. 둘은 대체 어떻게 만나야 할까?
“음… 일단 develop 브랜치에 코드가 push 되면, 저 왼쪽 상자, GitHub Actions Runner가 생기는 거지?”
“맞아. 깃허브가 우리를 위해 일해줄 일꾼을 하나 만들어주는 거야. 그 일꾼은 방금 네가 push한 최신 코드를 들고 있어.”
“그리고 그 코드를 오른쪽 상자, 미니PC에 전달해야 하고. 그럼… 그냥 미니PC의 주소를 알려주고 Runner한테 그리로 가라고 하면 되는 거 아니야?”
솔라는 가장 단순한 방법을 떠올렸다. 그녀의 생각 속에서 배포는 여전히 ‘git pull’의 자동화 버전이었기 때문이다.
“미니PC의 주소라. 가령 우리 집 인터넷의 공인 IP 주소와 포트 번호 같은 거?”
루나의 질문에 솔라는 당연하다는 듯 고개를 끄덕였다. 하지만 말을 내뱉는 순간, 뭔가 잘못되었다는 느낌이 스쳤다.
“어… 잠깐만. 그렇게 하면… 그 IP 주소와 포트 번호를 아는 사람은 누구나 우리 집 미니PC에 접속을 시도할 수 있겠네?”
“그렇지. 우리 집 현관문을 열어놓고 ‘배달 기사님만 들어오세요’라고 종이에 써 붙여두는 거랑 비슷해. 배달 기사님도 들어올 수 있지만, 원치 않는 손님도 얼마든지 들어올 수 있는 상태가 되는 거야.”
루나의 비유에 솔라는 얼굴을 찡그렸다. 개발용 테스트 서버라고는 하지만, 집 네트워크에 연결된 기기의 문을 활짝 열어놓는다는 건 생각만 해도 끔찍했다. 자신의 단순한 생각이 얼마나 위험한 것이었는지 깨달았다. git pull 자동화라는 생각에만 사로잡혀 ‘어떻게 안전하게 연결할 것인가’라는 가장 중요한 문제를 놓치고 있었다.
“그럼 안 되겠다. 뭔가… 우리만 아는 비밀 통로가 필요하겠네.”
“바로 그거야.”
루나는 빙긋 웃으며 두 상자 사이에 구불구불한 터널을 그렸다. 그리고 터널의 입구와 출구에 각각 자물쇠를 그려 넣었다.
“이 터널이 바로 Tailscale 같은 가상 사설망(VPN)이야. 인터넷이라는 공공도로 위에 우리만 쓸 수 있는 전용 지하도를 만드는 거지. Runner와 미니PC는 각자 이 지하도로 들어올 수 있는 열쇠를 갖고 있어. 그래서 공공도로에 자신을 노출하지 않고도 서로 만날 수 있는 거야.”
솔라는 그림을 보며 고개를 끄덕였다. 이제야 develop push는 Tailscale을 거쳐 미니PC에 라는 문장의 첫 부분이 구체적인 그림으로 보이기 시작했다.
“아하! 그러니까 GitHub Actions Runner는 인터넷 어딘가에 있지만, Tailscale을 통해 우리 집 미니PC와 같은 사설 네트워크 안에 있는 것처럼 행동할 수 있게 되는 거구나. 그래서 미니PC의 공인 IP가 아니라, 그 사설 네트워크 안에서만 통하는 내부 주소로 안전하게 SSH 접속을 할 수 있는 거네.”
“정확해. 한 걸음 더 나아가 볼까? 여기서 쓰는 열쇠는 심지어 일회용이야.”
루나는 왼쪽 상자, Runner 옆에 그려진 열쇠를 콕 집어 말했다.
“배포가 시작될 때마다 Runner는 tag:ci라는 특별한 꼬리표가 붙은 임시 열쇠를 발급받아. 이 열쇠로는 우리 네트워크의 다른 중요한 곳은 갈 수 없고, 오직 배포 대상인 미니PC에만 접속할 수 있지. 그리고 배포가 끝나면 그 열쇠는 바로 파기돼.”
그제야 솔라는 모든 조각이 맞춰지는 것을 느꼈다. develop 브랜치에 연결된 릴리즈 레일은 단순히 코드를 옮기는 길이 아니었다. 그것은 ‘외부의 불특정 공간(GitHub Runner)에서 내부의 사적인 공간(미니PC)으로, 안전하고, 일시적이며, 통제된 방식으로 코드를 전달하기 위한 매우 섬세하게 설계된 보안 경로’였다. 단순한 git pull 자동화와는 비교할 수 없는 수준의 시스템적 고민이 담겨 있었다.
솔라는 언니가 그린 그림을 가만히 들여다보았다. 처음에는 그저 단어의 나열로 보였던 문장이, 이제는 역할과 책임을 가진 행위자들이 서로 상호작용하는 하나의 완성된 장면으로 보였다.
“알겠다… develop 레일은 속도나 편의성만을 위한 게 아니었어. 외부 노출 없이 안전하게 테스트 환경을 유지하기 위한 거였구나.”
솔라는 이제 develop 레일에 대해서는 자신만의 분석틀을 갖게 된 기분이었다. 어떤 배포 경로든, 출발지와 목적지, 그리고 그 사이의 연결 방식과 보안 수준을 따져봐야 한다는 것을 깨달았다.
그런데 문득, 새로운 의문이 고개를 들었다. 이토록 완벽해 보이는 Tailscale이라는 비밀 통로가 있는데, 왜 main 브랜치는 다른 길을 택했을까?
“언니, 그럼 이상한데. 미니PC처럼 사적인 공간을 보호하기 위해 이 비밀 통로를 쓰는 건 완벽히 이해했어. 그런데 실제 사용자들이 쓰는 main 서버는 GCP에 있잖아. 어차피 처음부터 외부 접속을 염두에 두고 만든 ‘공적인 공간’ 아닌가? 그런데 왜 그쪽으로 가는 길이 더 복잡한 OIDC니 IAP니 하는 것들로 겹겹이 막혀있는 거야? 거기도 그냥 Tailscale 쓰면 안 돼?“
3장: GCP 운행 열차: main 레일의 보안 터널
솔라의 질문이 가라앉은 자리, 루나는 이전 장에서 그렸던 다이어그램을 그대로 두었다. ‘GitHub Actions Runner’와 ‘미니PC’를 잇던 구불구불한 Tailscale 터널. 루나는 그 그림 옆에 새로운 캔버스를 펼쳤다. 그리고 다시 한번 왼쪽에는 ‘GitHub Actions Runner’를, 오른쪽에는 ‘GCP VM (운영 서버)’라는 상자를 그렸다.
이번에는 둘 사이에 터널을 그리는 대신, 4개의 관문이 늘어선 길을 그렸다. 마치 고속도로 톨게이트처럼, 하나의 관문을 통과해야만 다음 관문이 나타나는 구조였다. 솔라는 자기도 모르게 화면으로 몸을 기울였다. develop 레일의 우아한 지하도와는 전혀 다른, 숨 막히는 검문소의 행렬이었다. 이 그림 자체가 솔라의 이전 질문—‘거기도 그냥 Tailscale 쓰면 안 돼?’—에 대한 무언의 대답처럼 느껴졌다. 공적인 공간은 사적인 공간과 다른 종류의 질서를 필요로 한다고 말하는 듯했다.
“develop 레일이 우리만 아는 비밀 통로를 만드는 거였다면, main 레일은 완전히 다른 문제를 풀어야 해.”
루나가 첫 번째 관문을 툭툭 건드리며 말했다.
“미니PC는 우리 소유의 사적인 공간이니까, 우리만 아는 비밀번호나 열쇠를 공유해도 괜찮았지. 하지만 GCP 같은 클라우드 환경은 수많은 사용자와 서비스가 함께 쓰는 거대한 데이터 센터야. 마치… 우리 집이 아니라, 수천 개의 사무실이 입주한 거대한 빌딩 같은 곳이지.”
“거대한 빌딩…”
솔라는 비유를 곱씹었다.
“그럼 거기에 우리 서버가 하나의 사무실처럼 입주해 있는 거고. Tailscale을 쓴다는 건, 그 빌딩의 다른 모든 사무실을 무시하고 우리 사무실로 통하는 비밀 복도를 하나 뚫는 것과 비슷한 건가?”
“비슷해. 가능은 하겠지만, 빌딩 전체의 보안 규칙과 충돌할 수 있고, 우리가 통제할 수 없는 변수가 많아져. 더 좋은 방법은 빌딩의 공식적인 보안 시스템을 이용하는 거야. 외부 방문객(GitHub Runner)이 우리 사무실(GCP VM)에 안전하게 들어오도록, 빌딩의 출입 절차를 따르는 거지.”
루나는 첫 번째 관문을 가리켰다. 거기에는 ‘GitHub OIDC’라고 적혀 있었다.
“첫 번째 관문은 신분증 발급 데스크야. 배포가 시작되면 GitHub은 Runner에게 아주 특별한 신분증(OIDC 토큰)을 발급해 줘. 이 신분증에는 ‘저는 OOO 프로젝트의 main 브랜치 배포 작업을 위해 파견된 정식 작업자이며, 이 신분증은 10분 후에 만료됩니다’ 같은 정보가 암호화되어 담겨있어.”
이어서 루나의 커서가 두 번째 관문, ‘GCP WIF’로 옮겨갔다.
“여기는 빌딩의 1층 로비에 있는 방문객 안내 데스크(Workload Identity Federation)야. 안내 데스크는 Runner가 내민 신분증을 보고, 이게 정말 GitHub 본사에서 발급한 진짜 신분증이 맞는지 전화해서 확인해. 만약 진짜라면, GCP 안에서만 통용되는 임시 출입증을 새로 내주지. 중요한 건, 이 과정에서 어떤 비밀번호도 오가지 않는다는 거야. 신분증의 진위 여부만 확인할 뿐이지.”
솔라는 자기도 모르게 가상 시나리오를 머릿속에 그리기 시작했다. 만약 이 과정에서 비밀번호나 SSH 키 같은 ‘만능 열쇠’를 썼다면? 그 키가 GitHub Actions 로그에라도 잠시 노출되는 날엔, 누군가 우리 운영 서버의 열쇠를 복제해가는 끔찍한 일이 벌어질 수도 있었다. 하지만 이 방식은 열쇠가 아니라, 매번 새로 발급되고 확인받는 ‘일회용 신분증’에 가까웠다.
“아… 열쇠를 복사할 위험이 아예 없네.”
“맞아. 그리고 세 번째 관문.” 루나는 ‘IAP’라고 쓰인 세 번째 관문을 강조했다. “출입증을 받았다고 빌딩 안을 마음대로 돌아다닐 수는 없어. 이 관문(Identity-Aware Proxy)은 우리 사무실로 향하는 딱 하나의 복도만 열어주는 보안 요원이야. Runner는 다른 곳은 기웃거릴 수도 없이, 오직 이 IAP 터널을 통해서만 우리 서버 문 앞까지 갈 수 있어.”
“마지막은… OS Login이네.”
솔라가 마지막 관문을 짚었다.
“응. 사무실 문 앞에 도착한 Runner가 아까 1층에서 발급받은 임시 출입증을 디지털 도어록(OS Login)에 대는 거야. 도어록은 그 출입증이 유효한지 다시 한번 확인하고 문을 열어주지. 배포가 끝나면? Runner와 그가 가졌던 모든 신분증, 출입증은 전부 파기돼. 흔적도 없이.”
관문 네 개를 모두 통과하자, 솔라는 긴 숨을 내쉬었다. 이것은 단순히 develop보다 ‘더 복잡한 인증’이 아니었다. 보안에 대한 철학 자체가 달랐다. develop 레일이 ‘신뢰하는 구성원만 들어오는 사적인 네트워크’를 전제로 했다면, main 레일은 ‘아무도 믿지 않는 제로 트러스트(Zero Trust) 환경’을 전제로 설계된 것이었다. 키나 비밀번호 같은 영속적인 비밀 정보를 시스템에서 완전히 제거하고, 매번 신원을 증명하고, 최소한의 권한만 부여하며, 모든 접근을 기록하는 방식.
솔라는 처음 봤던 그 문장을 다시 떠올렸다.
main 병합 시 GitHub OIDC와 GCP Workload Identity Federation으로 인증해 IAP·OS Login으로 운영 VM에 접속하도록 두 workflow를 만들었다.
이제 이것은 복잡한 기술의 나열이 아니었다. 외부 행위자(Runner)에게 운영 서버의 키를 단 하나도 넘겨주지 않고, 안전하게 작업을 위임하기 위한 치밀한 연쇄 반응의 설계도였다. 운영 환경의 보안은 단순히 문을 더 단단히 잠그는 문제가 아니라, ‘누가, 무엇을 근거로, 어디까지, 얼마 동안’ 접근할 수 있는지를 정의하는 시스템의 문제였던 것이다.
“알겠다… 왜 Tailscale을 안 썼는지. main 레일의 목표는 사적인 통로를 만드는 게 아니었어. 공적인 공간에서, 누구도 영원히 신뢰하지 않는다는 원칙 아래, 안전하게 임무를 수행할 경로를 만드는 거였구나.”
솔라의 시선은 루나가 그린 두 개의 다이어그램—단순한 터널과 겹겹의 관문—을 오갔다. 이제 그녀는 두 레일의 구조적 차이를 명확히 이해할 수 있었다. 브랜치는 정말로, 전혀 다른 세상으로 향하는 철로의 분기점이었다.
문득 새로운 궁금증이 생겼다. 이렇게 다른 길을 통해 목적지에 도착한 두 열차는, 그 다음엔 무엇을 할까?
“언니, 그럼 develop행 열차랑 main행 열차는 목적지에 도착해서 하는 일은 똑같아? 그냥 가져온 코드 내려놓고, 서버 재시작하고 끝? 아니면… 그 화물을 부리는 과정에도 뭔가 다른 점이 있는 거야?“
4장: 레일 위 화물: 공통 배포 스크립트와 안전장치
솔라의 질문은 두 개의 서로 다른 철로, develop 레일과 main 레일에 대한 마지막 의문이었다. 도착지가 달랐던 두 열차는, 화물을 내려놓는 방식도 다를까?
루나는 대답 대신, 화면에 나란히 놓인 두 개의 다이어그램—Tailscale 터널이 그려진 미니PC행 경로와, 여러 개의 관문이 그려진 GCP행 경로—을 조용히 수정하기 시작했다. 그녀는 각 경로의 최종 목적지인 ‘미니PC’와 ‘GCP VM’ 상자 바로 옆에, 동일한 내용이 적힌 새로운 상자를 하나씩 추가했다. 그리고 각 레일의 끝에서 뻗어 나온 화살표가 이 새로운 상자를 가리키도록 연결했다.
상자 안에는 체크리스트처럼 보이는 항목들이 순서대로 나열되어 있었다.
[배포 스크립트]
1. 동시 배포 잠금(Lock) 확인
2. 추적 파일 변경 여부 확인
3. 데이터베이스 백업 (PostgreSQL)
4. 이미지 빌드 (Docker)
5. 데이터베이스 마이그레이션 (Alembic)
6. 컨테이너 갱신
7. 헬스 체크
솔라는 눈을 가늘게 뜨고 목록을 훑었다. 그녀의 생각 속 배포는 ‘코드 복사 후 서버 재시작’이라는 두 단계로 끝나는 단순한 과정이었다. 하지만 눈앞의 목록은 훨씬 더 복잡하고, 어쩐지 번거로워 보였다.
“언니, 이 목록은… develop이랑 main 둘 다 똑같이 적용되는 거야? 그냥 내 컴퓨터에서 테스트하는 거나 다름없는 미니PC에 배포할 때도 데이터베이스 백업까지 한다고? 이건 좀… 과한 것 같은데.”
솔라의 목소리에는 솔직한 의구심이 묻어났다. 그녀에게 배포 스크립트는 단순히 반복적인 명령어를 모아놓은, 편의를 위한 도구일 뿐이었다. 저렇게 많은 단계를 거치는 것은 그저 시간을 낭비하는 것처럼 느껴졌다.
루나는 솔라의 말을 부정하지 않았다. 대신, 마우스 커서로 목록의 한가운데를 가리켰다.
“좋아, 솔라. 그럼 이 과정이 정말 안전하고 튼튼한지 한번 흔들어보자. 만약 네가 이 스크립트를 더 효율적으로 만든다면서 여기서 딱 두 단계만 뺀다면, 뭘 빼고 싶어?”
솔라는 잠시 고민했다. 가장 복잡해 보이고, 가장 시간이 오래 걸릴 것 같은 단계들이 눈에 들어왔다.
“음… 3번 ‘데이터베이스 백업’이랑 5번 ‘데이터베이스 마이그레이션’. 코드만 살짝 고쳤을 뿐인데 매번 데이터베이스를 백업하고 구조 변경을 시도하는 건 낭비 같아. 필요할 때만 수동으로 하면 안 돼?”
“좋은 선택이야.” 루나는 말과 동시에 목록에서 그 두 항목을 희미하게 회색으로 칠했다. “자, 이제 우리만의 ‘최적화된’ 배포 스크립트가 완성됐어. 이 상태에서 네가 사용자 정보에 ‘나이’ 필드를 추가하는 코드를 작성해서 develop 브랜치에 push했다고 상상해 봐. 어떤 일이 일어날까?”
솔라는 머릿속으로 시뮬레이션을 돌리기 시작했다.
- GitHub Actions Runner가 새 코드를 받는다.
develop레일을 타고 미니PC에 접속한다.- ‘최적화된’ 스크립트를 실행한다. 백업과 마이그레이션은 건너뛴다.
- 새로운 코드가 담긴 컨테이너가 실행된다.
“음… 일단 앱은 새로 뜨겠지. 그리고 내가 새 기능을 테스트하려고 사용자 정보 페이지에 들어가면…”
거기까지 생각한 솔라의 표정이 굳었다.
“아. 앱이 터지겠네.”
새로운 코드는 데이터베이스의 users 테이블에 age라는 컬럼이 당연히 있을 거라고 기대하며 쿼리를 날릴 것이다. 하지만 5번 마이그레이션 단계를 건너뛰었기 때문에, 실제 데이터베이스에는 age 컬럼이 존재하지 않는다. 결과는 명백한 서버 오류였다.
“맞아. 서비스가 중단되지. 데이터가 사라지진 않았지만, 아무도 서비스를 쓸 수 없는 상태가 되는 거야. 그럼 이번엔 반대로 상상해 볼까? 마이그레이션은 그대로 두고, ‘백업’만 빼보자.”
루나는 ‘데이터베이스 마이그레이션’ 항목의 색을 원래대로 되돌리고 ‘데이터베이스 백업’만 회색으로 남겨두었다.
“네가 만든 마이그레이션 스크립트에 작은 버그가 있었어. age 컬럼을 추가하면서, 실수로 모든 사용자의 email 정보를 null로 업데이트하는 코드가 들어간 거야. 배포는 어떻게 될까?”
솔라는 다시 한번 상상했다. 앱은 정상적으로 뜬다. 심지어 7번 헬스 체크도 통과할지 모른다. 앱이 죽지는 않았으니까. 하지만 그 뒤에 벌어질 일은 끔찍했다.
“모든 사용자가 로그인을 못 하게 되겠네. 이메일 정보가 다 날아갔으니까. 그리고… 그걸 되돌릴 방법이 없어. 백업을 안 했으니까.”
솔라는 등골이 서늘해지는 것을 느꼈다. 자신이 ‘불필요한 낭비’라고 생각했던 단계들이, 사실은 서비스의 안정성과 데이터의 무결성을 지키는 마지막 안전망이었던 것이다. 배포 스크립트는 편의를 위한 자동화가 아니라, 잠재적인 재앙을 막기 위해 촘촘하게 짜인 방어 절차였다.
“하나 더.” 루나는 목록의 첫 번째 항목, ‘동시 배포 잠금(Lock) 확인’을 콕 집었다. “이건 왜 맨 앞에 있을까? 너랑 내가 딱 1초 차이로 각자 다른 기능을 develop 브랜치에 push했다고 생각해 봐. 잠금 장치가 없다면?”
솔라는 두 개의 배포 스크립트가 한 서버 위에서 동시에 돌아가는 장면을 떠올렸다. 한쪽이 데이터베이스를 백업하는 동안 다른 한쪽이 마이그레이션을 시도하고, 한쪽이 컨테이너를 내리는 동안 다른 한쪽은 헬스 체크를 하는 아수라장. 서버는 어느 장단에 맞춰야 할지 모른 채 뒤죽박죽이 될 것이다.
“서버가… 더러워지겠네. 어떤 코드가 실행 중인지, 데이터베이스 상태는 어떤지 아무도 확신할 수 없는 상태. 그걸 덮어쓰지 않기 위한 장치였구나.”
이제 솔라는 루나가 그려준 목록을 완전히 다른 눈으로 바라보았다. 이것은 단순한 명령어의 나열이 아니었다. 시스템을 가장 취약한 순간, 즉 ‘변경’의 순간에 보호하기 위한 외과 수술 프로토콜과 같았다.
백업은 만일의 사태를 대비한 마취와 혈액 확보, 마이그레이션은 정교한 수술, 헬스 체크는 환자의 바이탈을 확인하는 과정. 그리고 동시 배포 잠금은 수술실에 오직 한 명의 집도의만 들어올 수 있게 하는 규칙이었다.
“알겠다… 배포는 그냥 코드를 서버에 던져놓는 게 아니었어. 시스템의 상태를 한 지점에서 다음 지점으로 ‘안전하게 전환’시키는 책임이 따르는 과정이었구나.”
이제 솔라에게 저 목록은 ‘배포 안정성 검증 체크리스트’처럼 보였다. 어떤 배포든, 이 항목들을 건너뛸 수 있는지 자문해야만 했다.
두 개의 다른 철로, 그리고 그 끝에서 기다리는 동일한 수술 프로토콜. 이제 거의 모든 그림이 맞춰졌다. 하지만 마지막 한 조각이 남아있었다.
“언니, 이제 develop 레일과 main 레일이 어떻게 다르고, 목적지에 도착해서 어떤 정교한 작업을 하는지 알겠어. 그럼 이 모든 지식을 합쳐서… 내가 앞으로 전혀 다른 목적지를 가진 세 번째, 네 번째 레일을 만들어야 할 때 이걸 어떻게 써야 하지? 이 모든 원칙을 그냥 다 적용하면 되는 걸까?“
5장: 릴리즈 레일: 브랜치를 넘어선 시스템 책임
솔라의 화면에는 지난 몇 시간 동안의 고민의 흔적이 고스란히 남아 있었다. 왼쪽에는 develop 브랜치에서 미니PC로 이어지는 구불구불한 Tailscale 터널 다이어그램이, 오른쪽에는 main 브랜치에서 GCP 운영 서버로 향하는 겹겹의 보안 관문 다이어그램이 있었다. 그리고 이제, 그 두 그림 옆에는 솔라가 막 추가한 세 번째 열이 자리 잡고 있었다. staging.
솔라는 곧 있을 대규모 업데이트의 사전 검증을 위해, 운영 환경과 거의 똑같은 staging 환경을 만들고 그곳으로 배포할 브랜치를 새로 파야 했다. 그녀는 feature/new-update 브랜치를 staging 브랜치에 병합했을 때 어떤 일이 일어나야 할지 그려보려 했다. 하지만 그녀의 캔버스 위 staging 레일은 어딘가 허전했다. ‘GitHub Actions Runner’에서 ‘Staging Server (GCP)’로 그어진 단순한 화살표 하나. 그게 전부였다. 마치 모든 것을 알기 전, 1장으로 돌아간 듯한 그림이었다.
솔직히 말해, 솔라는 약간의 반항심으로 이 그림을 그렸다. develop과 main, 두 개의 레일을 구축하는 데 들어간 복잡성을 이해하고 나니, 또 하나의 레일을 만드는 것이 거대한 부담처럼 느껴졌다. ‘이것도 저것도 다 저렇게 복잡하게 만들어야 한다면, 도대체 개발은 언제 하지? 이건 좀 과한 것 같아.’ 그녀의 머릿속에는 ‘오버엔지니어링’이라는 단어가 맴돌았다.
그때, 솔라의 어깨너머로 화면을 들여다보던 루나가 조용히 입을 열었다.
“새로운 철로를 설계하고 있네.”
루나는 솔라의 허전한 다이어그램을 비난하지 않았다. 대신, 그 그림이 제기하는 질문의 핵심을 짚었다.
“솔라, 네가 지금 만들려는 이 staging 브랜치, 이 코드가 최종적으로 도착해야 할 목적지는 어떤 곳이야? 그 환경의 성격이 뭐지?”
“음… QA팀이 최종 테스트를 하기 위한 곳이야. 운영 환경이랑 거의 똑같은 복제본 같은 거지.”
“좋아. 그럼 그 QA팀은 우리 집 공유기 안에 있어? 아니면 재택근무를 하거나 다른 사무실에 있어?”
루나의 질문은 단순했지만, 솔라의 논리의 가장 약한 고리를 정확히 찔렀다.
“당연히… 밖에 있지.”
“그럼 그 서버는 인터넷 어딘가에서 접근할 수 있어야겠네. 마치 main 서버처럼.”
솔라는 아무 말도 할 수 없었다. develop 레일이 Tailscale이라는 폐쇄적인 터널을 쓸 수 있었던 이유는, 그 목적지인 미니PC가 ‘사적인 공간’에 있었고 접근하는 주체도 ‘우리’로 한정되었기 때문이다. 하지만 staging 서버는 불특정한 외부(QA팀)의 접근을 허용해야만 했다. 그 본질이 main 서버와 똑같이 ‘공적인 공간’에 노출되어 있음을 의미했다.
루나는 한 걸음 더 나아갔다.
“하나 더. staging 환경에서 데이터베이스 마이그레이션이 잘못되어 데이터가 전부 엉망이 됐다고 상상해 봐. 그 위험의 무게가, 네 미니PC에 있는 테스트 데이터가 날아가는 거랑 같을까?”
아니, 전혀 같지 않았다. staging 환경의 데이터는 비록 실제 운영 데이터는 아닐지라도, 운영 데이터베이스의 스키마를 그대로 복제하고 그 위에서 온갖 테스트를 거친다. 그곳에서의 실패는 곧 운영 환경에서 일어날 수 있는 실패를 예고하는 것과 같다. develop 환경의 실패가 ‘개인의 불편함’ 수준이라면, staging 환경의 실패는 ‘프로젝트의 재앙’에 가까웠다.
순간 솔라의 머릿속을 맴돌던 ‘오버엔지니어링’이라는 단어가 눈 녹듯 사라졌다. 그녀는 부끄러움을 느꼈다. develop 레일의 단순함과 main 레일의 복잡함 사이에서, 기술적인 ‘메뉴’를 고르려고만 했다. Tailscale이 편해 보이면 그걸 쓰고 싶고, OIDC가 복잡해 보이면 피하고 싶었다. 하지만 그것은 완전히 잘못된 접근이었다.
“아…”
탄식이 흘러나왔다.
“어떤 기술을 쓸지부터 고민하는 게 아니었어. 이 브랜치가 책임져야 할 환경이 어떤 성격을 갖는지부터 물었어야 했구나.”
솔라의 시선이 자신의 허전한 staging 다이어그램으로 향했다. 이제 그 그림은 더 이상 반항의 산물이 아니라, 풀어야 할 명확한 문제지가 되었다.
“첫째, 목적지는 어디인가? 사적인 공간인가, 공적인 공간인가? 둘째, 그곳까지 가는 경로는 어떻게 보호해야 하는가? 신뢰할 수 있는 내부자인가, 아니면 증명이 필요한 외부자인가? 셋째, 그곳에 도착해서 어떤 책임을 져야 하는가? 실패의 대가가 큰가, 작은가?”
마치 스스로에게 다짐하듯, 솔라는 세 가지 원칙을 중얼거렸다. 그것이 바로 그녀가 찾던 ‘릴리즈 레일 설계 원칙’이었다. 브랜치를 새로 만들 때마다 따라야 할, 기술 선택에 앞서는 근본적인 질문들이었다.
솔라는 미련 없이 허전한 화살표를 지웠다. 그리고 main 레일의 다이어그램을 가져와 그 옆에 복사했다. 그녀는 더 이상 복잡함에 주눅 들지 않았다. 각 관문이 왜 필요한지, 그 책임의 무게를 이해했기 때문이다.
“좋아, staging 레일 다시 설계하자.”
그녀의 손가락이 키보드 위에서 경쾌하게 움직였다.
“출발점은 staging 브랜치. 목적지는 GCP에 있는, 인터넷에 노출된 서버. 따라서 경로는 외부 접근을 안전하게 통제할 수 있는 OIDC와 IAP 연동 방식이 맞아. 그리고 책임… 실패 비용이 운영 환경과 거의 같으니, 백업, 마이그레이션, 헬스 체크를 포함한 전체 배포 스크립트를 그대로 적용해야 해.”
이제 그녀의 머릿속에서 git branch staging 이라는 명령어는 단순히 새로운 코드 저장소를 만드는 행위가 아니었다. 그것은 새로운 목적지를 향한 철도를 놓는, 시스템 전체의 책임을 짊어지는 설계의 첫 삽을 뜨는 것과 같았다.
처음 솔라를 혼란스럽게 했던 문장, develop push는 ... main push는 ... 이라는 문장은 이제 더 이상 복잡한 사실의 나열이 아니었다. 그것은 각기 다른 책임을 지닌 두 시스템에 대한 선언문이었다. 그리고 솔라는 이제, 그 옆에 당당하게 세 번째 선언문을 쓸 준비가 되어 있었다.
staging push는...
솔라는 텅 빈 캔버스를 채워나가기 시작했다. 그녀는 더 이상 길 위에서 헤매는 승객이 아니었다. 이제 막 자신의 첫 철도를 설계하기 시작한, 신입 엔지니어였다.