장찬욱
우리는 2개의 서버를 돌리고 있다. 하나는 실제 방문자가 보는 서버(운영, prod), 다른 하나는 우리끼리 미리 써보는 서버(스테이징, stage)다. 배포 과정에서 개발자들이 느끼는 부담을 덜기 위해 CI/CD 파이프라인도 함께 구현했다.
미리 보는 용어
- CI/CD — 코드 변경을 자동으로 검증하고 배포까지 이어주는 자동화 체계
- 컨테이너 — 한 컴퓨터 안에서 프로그램을 서로 독립된 상자처럼 나눠 돌리는 기술
- 브랜치 / PR / 머지 — 깃허브에서 코드를 나눠 작업하고(브랜치), 합쳐달라고 요청하고(PR), 실제로 합치는(머지) 흐름
- 빌드 / 이미지 — 코드를 실행 가능한 형태로 조립하고(빌드), 그 결과물을 통째로 포장한 파일(이미지)
- 배포 태그 — 그 순간 배포된 코드 버전에 붙이는 이름표
- 회귀 — 고치려던 것과 무관한 다른 기능이 망가지는 것
인프라·CI/CD는 나 혼자 맡고 있었고, 이 홈페이지를 만드는 팀은 8명(그중 개발자는 6명), 동아리 전체로 보면 약 50명이 사용할 것이 예상되는 조직이었다. 인프라를 만들기 시작하던 시점엔 이 규모를 딱히 염두에 두지 않았다. 그저 어떤 인프라를 구현해볼까, 어떻게 구현할까 하는 기술적인 고민이 앞섰다. 처음에는 표준으로 알려진 구조부터 찾아봤다. 찾아보면 대체로 답이 같았다 — 개발(dev), 검증(QA), 운영(prod)을 세 단계로 나누는 구조. 어느 정도 규모가 있는 조직, 특히 소프트웨어 중심 회사라면 흔히 쓰는 방식이라 처음엔 이걸 그대로 가져가려고 했다.
그런데 PM과의 협의 과정에서, 내가 놓치고 있던 부분을 깨닫게 됐다. dev/QA/prod 3단 분리를 전제로 이야기를 진행했는데, PM은 그 정도 인프라가 정말 필요한지부터 되물었다 — 셋으로 나눴을 때 드는 비용과, 프로덕션 하나만 뒀을 때 우리가 얻는 이득을 비교해보라는 것이었다. 나중에 인프라·CI/CD를 어느 정도 구축하고 팀에 설명하는 자리에서는, 안정성을 당연한 우선순위로 두고 있으나 PM이 "가끔 서버가 내려가도 된다"는 사실을 알려줬다. 홍보 사이트가 미션 크리티컬하지 않다는 점을 알게 되어 서비스 자체에 대한 이해도가 인프라에 있어서도 아주 중요하다는 것을 느꼈다. 혼자 판단했다면 못 봤을 시야를 더해준 셈이라, 고마운 지점이었다.
그래서 원래 그리던 구조를 하나씩 내려놓았다. 내 질문은 이렇게 변했다: "표준적으로 어떤 구조를 갖춰야 하는가"에서 "지금 우리에게 주어진 조건이 무엇인가?", "그렇다면 그 조건(무료 티어, 소규모 팀) 안에서 인프라를 어떻게 구성해야 하는가"로.
먼저 dev와 QA를 두는 이유는 다음과 같다. dev는 개발자가 자유롭게 실험하고 자주 부수면서 기능을 만드는 공간이 필요해서고, QA는 여러 사람의 코드가 합쳐진 결과를, dev처럼 계속 흔들리는 환경이 아니라 운영과 비슷한 안정된 상태에서 통합 검증할 공간이 필요해서다. 즉 두 환경은 목적 자체가 다르기 때문에 나뉘어 있는 것이다.
그러나 우리 조직 규모상 위 조건들에 해당하지 않았다. 개발자는 4~6명이었고, 배포 빈도도 낮아서 dev와 별개로 안정된 통합 검증 단계를 둘 실익이 크지 않았다. 환경을 하나 더 두는 만큼 관리할 앱·DB·배포 파이프라인·확인 대상만 늘어났다.
그러나 그렇다고 하더라도 prod는 무조건 격리되어야 했다. 방문자가 실제로 보는 화면과 실제 데이터가 거기 있었다. 개발 중인 코드가 실수로라도 섞이면 방문자가 깨진 화면을 보거나 데이터가 잘못 저장될 수 있었다. "가끔 내려가도 된다"는 안정성에 과하게 투자하지 않아도 된다는 뜻이었지, 개발 중인 걸 그대로 노출해도 된다는 뜻은 아니었다.
그러므로 현재의 stage / prod 구조를 채택하기로 했다. 우리 규모에 더 맞는 형태라고 판단했다.
이렇게 dev와 QA를 하나로 합친 만큼, 운영 방식도 그에 맞게 조정했다. 팀 내부적으로는 stage에 올라간 변경사항을 그때그때 바로 prod로 승격하지 않고, 어느 정도 검증이 쌓이면 그걸 한 번에 모아 prod에 반영하는 식으로 운영했다.
그렇게 다음 질문으로 이어졌다. "그럼 그 두 환경을 어떻게 격리하여 운영할 것인가?"
서버를 두 개 두려고 컴퓨터를 두 대 빌리면 돈이 두 배로 든다. 우리는 트래픽이 아직 많지 않았고, 무료 티어 안에서 버텨야 했기 때문에, 한 대의 컴퓨터 안에서 stage와 prod를 나눠 띄우기로 했다. 여기서 선택지는 두 가지였다 — 도커 컨테이너로 감싸거나, systemd 서비스로 직접 띄우거나. systemd 쪽은 배포할 때마다 서버에 접속해 커맨드를 직접 치거나, 그 과정을 별도 스크립트로 자동화해야 했다. 도커는 이미지를 빌드해서 pull·run하는 것만으로 배포가 끝나서, 마침 만들려던 CI/CD 자동화 파이프라인에 그대로 얹을 수 있었다. 그래서 도커 컴포즈로 컨테이너 두 개(stage용, prod용)를 띄우는 쪽을 택했다. 둘은 서로 다른 포트, 다른 데이터베이스 파일, 다른 배포 태그를 쓴다 — 이름만 stage/prod로 나뉜 게 아니라 실제로 데이터가 안 섞이고, 한쪽을 재배포해도 다른 쪽엔 영향이 없게 만들었다.
이렇게 stage/prod라는 두 형상을 나누고 나니, 다음 질문은 이 둘의 버전을 어떻게 관리하느냐였다. dev 브랜치의 상태를 그대로 stage로, main 브랜치의 상태를 그대로 prod로 삼기로 했다. prod는 stage의 모든 코드를, 클라우드 환경에서 수동으로 써보며 검증된 뒤에야 물려받을 수 있어야 했다. 그러다 보니 PR은 자연스럽게 한 방향으로만 흐르게 됐다 — 각자의 개발 브랜치에서 dev로, dev에서 main으로만.
두 서버를 어떻게 나눠서 배포할지 정할 때, 먼저 떠올린 건 PR마다 그때그때 환경을 새로 띄우는 방식이었다. 하지만 지금 우리 팀 수준에서는 비현실적이라고 판단했다 — 그런 자동화를 제대로 만들려면 걸리는 시간이 사이트를 만드는 시간이 더 길어질 수 있었고, 무료 티어 서버 자원(2 vCPU, 12GB RAM)도 여러 버전을 동시에 띄울 만큼 넉넉하지 않았다.
그래서 관리 복잡성과 구현에 드는 시간을 함께 고려해, stage와 prod 각각 딱 한 버전만 띄우기로 정했다. 배포 태그로 환경을 따로 관리하는 방법도 생각해봤지만, 태그를 일일이 관리하고 계속 최신 상태로 맞춰야 한다는 게 번거롭게 느껴졌다. dev와 main은 어차피 지우지 않고 계속 살아있는 브랜치니까, 그 브랜치 자체를 환경의 대표로 삼으면 태그 관리 부담이 그만큼 줄어든다 — 브랜치 상태가 곧 그 환경의 상태였다. 그래서 규칙은 단순하게 정리됐다: dev 브랜치에 머지되면 stage로, main 브랜치에 머지되면 prod로. 여기에 더해 main으로 가는 PR은 반드시 dev에서만 열기로 팀 내부 규칙으로 정했다 — 별도 설정으로 강제한 건 아니고 약속이었지만, prod에 반영되는 코드는 항상 한 번 stage를 거쳐 검증된 코드여야 main의 코드 품질이 유지된다고 봤다.
자원 문제도 같은 맥락에서 미리 손봐뒀다. stage와 prod가 한 컴퓨터를 나눠 쓰는 이상, 컨테이너별로 메모리 상한을 걸어 한쪽이 넘치게 써서 다른 쪽까지 끌고 내려가는 상황은 막아야 했다.
이제 개발자들은 PR을 올리고 리뷰를 거쳐 머지하게 됐다. 그 과정에서 빌드, 테스트, 배포는 명백히 반복되는 일들이다. 기능을 개발하면 빠르게 배포 가능하도록 만들기 위해서 본격적인 개발 전에 CI/CD 파이프라인을 만들어두는 게 좋겠다고 판단했다.
CI는 낯설지 않았다 — 다른 프로젝트에서 PR이 올라오면 GitHub Actions가 트리거돼서 빌드하고 테스트하는 흐름은 이미 써본 경험이 있었다. 문제는 CD였다. 빌드가 끝난 이미지를 가져와서 실제 서버에 배포하는 일. 이건 처음이었고, 가장 먼저 막힌 질문은 "이걸 언제 해야 하는가"였다 — PR이 올라올 때마다? 머지가 될 때? 그것도 어느 브랜치로 머지될 때?
브랜치에 머지된 코드는 그대로 그 환경에서 도는 코드가 된다. 그렇다면 머지되는 순간엔 이미 그 코드가 믿을 만해야 했다. 그래서 내린 결론은 PR이 올라올 때, 머지가 됐을 때로 나눠서 생각하는 방식이었다. CI(빌드·테스트)는 PR이 열릴 때마다 돌게 뒀다 — dev 브랜치엔 단위·통합 테스트를 통과한 코드만, 최소한 회귀는 없는 코드만 머지되게 하려는 목적이었다. CD(배포)는 자연스럽게 그다음이었다. 코드 리뷰를 거쳐 머지가 됐다는 건 이미 그 테스트를 모두 통과했다는 뜻이고, 그럼 그 코드는 stage라는 실제 클라우드 환경에서 검증받을 자격을 얻은 셈이었다. 그래서 배포를 담당하는 CD 파이프라인은 머지가 되는 순간에 자연스럽게 이어지도록 했다.
실제로 파이프라인을 돌리기 시작하자 예상 못한 문제들이 하나둘 나왔다 — 배포는 성공으로 표시됐는데 서버는 정작 죽어있던 적도 있었고, 앱은 최신 버전이 반영됐는데 막상 데이터베이스 구조 변경이 반영 안 된 채로 조용히 남아있던 적도 있었다. 전체 파이프라인을 써보면서 어느 지점에서 뭘 고민해야 하는지 하나씩 알게 됐고, 그때마다 개선해나가는 과정의 연속이었다. 지금은 훨씬 안정적으로 파이프라인이 동작하고 있다.
개발자들이 PR을 올리면 CI가 작동하고, dev에 머지되면 stage가, main에 머지되면 prod가 각자 독립적으로 배포된다. 두 서버는 서로 다른 데이터베이스 파일을 쓰고 자원도 독립적으로 상한이 걸려있다. stage에서 급하게 실험하다 데이터가 엉켜도 prod는 영향이 없다. 반대도 그렇다.
결국 서버 두 개도, CI/CD도 화려해서 고른 게 아니라 우리 규모에 딱 맞아서 남은 것들이다.
이렇게 전체 파이프라인을 구성하는 과정에서, 처음 구상에는 생각하지 못했던 검증 단계들이 여러 문제를 겪으며 하나씩 추가되었는데, 이야기할 거리가 많아서 각각 다른 글에서 다루려고 한다. 이외에도 인프라를 어떻게 구성하고, 개발자들의 요구사항을 받아 개선했는지도 다룰 예정이다.