혼자 개발하는데 CI가 왜 필요했나 — 편의 기능인 줄 알았는데 임계 경로였습니다
어느 날 아침, 제 저장소의 CI가 전부 멈췄습니다. 그리고 저는 그날에서야 제 프로젝트에서 CI가 무엇이었는지를 정확히 알게 됐습니다. 그전까지 저에게 CI는 테스트를 대신 돌려주는 편리한 도구였습니다. 막상 멈춰보니 그게 아니었습니다. 저는 아무것도 합칠 수 없는 상태가 되어 있었습니다.
이 글은 그 뒤로 2주 동안 집에서 놀던 노트북 두 대를 CI 서버로 만들었다가 결국 전부 꺼버린 이야기의 첫 편입니다. 다만 그 이야기를 시작하기 전에, 왜 혼자 개발하는 사람이 거기까지 갔는지부터 적어두는 게 순서라는 생각이 들었습니다.
CI는 무엇인가
CI(Continuous Integration, 지속적 통합)는 이름이 거창하지만 하는 일은 단순합니다. 코드를 올릴 때마다 빌드와 테스트를 자동으로 돌려서, 합치기 전에 깨진 것을 찾아내는 장치입니다.
원래는 사람이 하던 일입니다. 코드를 고쳤으면 빌드를 해보고, 테스트를 돌려보고, 문제가 없으면 합칩니다. 문제는 사람이 이걸 매번 빠짐없이 하지 않는다는 데 있습니다. 급할 때는 건너뛰고, 작은 수정이라고 생각하면 그냥 넘어갑니다. 그렇게 넘어간 것들이 나중에 한꺼번에 터집니다. CI는 그 판단을 사람에게서 회수해서 기계에 맡기는 것입니다. 빠뜨릴 수 없게 만드는 것이 핵심이지, 기술적으로 대단한 것은 아닙니다.
그래서 흔히 나오는 반문이 있습니다. 팀이 있으면 몰라도, 혼자 개발하는데 그게 필요하냐는 것입니다. 저도 한동안 그렇게 생각했습니다.
혼자인데도 필요했던 세 가지 이유
제 저장소는 모노레포입니다. 개인 블로그에서 시작했지만 지금은 여러 서비스가 하나의 백엔드를 공유하는 구조로 자랐습니다. 이 구조에서는 한 곳을 고치면 제가 예상하지 못한 곳이 깨집니다. 공유 타입을 하나 바꾸면 그것을 쓰는 프론트엔드 세 곳이 동시에 영향을 받습니다. 혼자라서 안전한 게 아니라, 혼자라서 그 영향 범위를 전부 기억하고 있을 수가 없습니다.
두 번째는 테스트가 무겁다는 것입니다. 백엔드 테스트 스위트는 6GB 힙을 요구하고, 영상 편집 서비스의 브라우저 E2E 테스트는 163건이 돌아갑니다. 이걸 개발 머신에서 돌리면 그동안 다른 작업을 할 수가 없습니다. 로컬에서 돌릴 수 있느냐가 아니라, 돌리는 동안 제 노트북이 마비되느냐가 실제 문제였습니다.
세 번째는 제 작업 방식입니다. 저는 여러 작업 브랜치를 병렬로 굴립니다. 그러다 보니 PR이 동시에 서너 개씩 열려 있는 일이 흔합니다. 각각이 서로 다른 시점의 main 위에 올라가 있고, 그중 하나가 머지되면 나머지의 전제가 바뀝니다. 이걸 사람이 추적하는 것은 현실적이지 않았습니다.
| 검증 대상 | 로컬로는 왜 부족한가 |
|---|---|
| 변경 패키지 선별 | 수동으로 판단하면 영향 범위를 빠뜨립니다 |
| 공유 타입 계약 | 백엔드와 프론트를 동시에 확인해야 합니다 |
| 백엔드 테스트 | 6GB 힙 — 도는 동안 개발 머신이 묶입니다 |
| 브라우저 E2E | 163개 시나리오, 십수 분이 걸립니다 |
여기까지가 제가 CI를 두고 있던 이유입니다. 정리하고 보니 전부 편의에 관한 것입니다. 손이 덜 가고, 빠뜨리지 않고, 노트북이 덜 묶입니다. 없으면 불편하지만 못 살 것은 아닌, 딱 그 정도의 물건이라고 저는 생각하고 있었습니다.
그런데 그것은 편의 기능이 아니었습니다
제 저장소에는 main 브랜치 보호가 걸려 있습니다. main에 직접 커밋할 수 없고, 모든 변경은 PR을 거칩니다. 그리고 그 PR을 머지할지 판단하는 게이트가 여러 조건을 확인하는데, 그중 하나가 CI 체크 통과 여부입니다.
이 구조가 평소에는 보이지 않습니다. CI가 초록불이면 게이트는 조용히 통과하고, 머지는 그냥 됩니다. 문제는 CI가 초록불이 아니라 아예 돌지 않을 때 드러납니다. 실패가 아니라 부재입니다. 게이트 입장에서 확인할 결과 자체가 오지 않으므로, 게이트는 통과시키지 않습니다.
그래서 CI가 멈춘 그날, 제가 잃은 것은 테스트 자동 실행이 아니었습니다. 머지가 멈췄고, 머지가 멈추니 배포가 멈췄습니다. 코드를 쓸 수는 있는데 어디에도 반영할 수 없는 상태였습니다. 돌이켜 생각해보면 저는 편의 기능이라고 부르던 것을 이미 오래전에 임계 경로 위에 올려두고 있었고, 그 사실을 그것이 끊어진 다음에야 알았습니다.
규모도 제 생각보다 컸습니다. 그 무렵 한 달 동안 이 저장소에서 돌아간 CI 실행은 500회가 넘었습니다. 하루에 열몇 번씩, 제가 의식하지 못한 채로 계속 돌고 있던 것입니다. 무언가가 얼마나 중요한지는 그것이 있을 때가 아니라 없을 때 드러난다는 말이 있는데, 저에게는 이 숫자가 그랬습니다.
그래서 노트북 두 대를 켜게 됐습니다
여기까지 이해하고 나면, 제가 그다음에 한 선택이 조금은 덜 이상해 보일 것입니다. CI가 막혔을 때 저에게는 두 가지 길이 있었습니다. 돈을 내고 뚫든지, 아니면 제 손으로 실행할 컴퓨터를 마련하든지.
저는 과금 없이 무료로 운영한다는 원칙을 갖고 있었습니다. 그래서 두 번째를 골랐고, 집에서 놀고 있던 노트북 두 대를 CI 서버로 만들기 시작했습니다. 결론부터 말씀드리면 그 두 대는 지금 전부 꺼져 있습니다. 다만 끄기까지 2주가 걸렸고, 그 사이에 제가 세운 진단은 대부분 틀렸습니다.
다음 편에서는 그 시작, 그러니까 CI가 멈춘 날 제가 무엇을 했고 무엇을 확인하지 않았는지부터 적겠습니다. 대응은 30분 만에 끝났는데, 정작 원인은 2주 뒤에야 알게 됩니다.
관련 글
CI가 전부 멈췄고, 저는 원인을 확인하지 않았습니다 — 30분 만에 끝난 대응의 함정
GitHub Actions hosted 러너가 새벽에 전면 차단됐습니다. 변수 한 줄과 self-hosted 러너로 30분 만에 우회했지만, 에러 메시지를 원인으로 착각한 탓에 진짜 이유는 2주 뒤에야 알게 됐습니다.
빌드는 CI에서, 실행은 서버에서: GHCR pull-only 배포
작은 서버는 앱을 실행할 여유는 있어도 빌드에서 죽습니다. 빌드를 GitHub Actions로 빼고 서버는 완성 이미지를 받아 실행만 하는 pull-only 구조와, private 레지스트리 인증까지 정리했습니다.
집에서 놀던 노트북 두 대를 CI 러너로 만들었습니다 — 라벨로 폴백을 만드는 법
맥북 한 대로는 CI 큐가 밀렸습니다. 방치돼 있던 Proxmox 홈랩 서버와 윈도우 노트북을 러너로 붙이면서, 네이티브 폴백이 없는 GitHub Actions에서 라벨만으로 폴백을 만든 과정을 정리합니다.