한 줄 정의
버전 관리(VCS)란 소스 코드와 ‘시간’의 상호작용을 관리하는 핵심 도구입니다. 파일시스템이 (이름 → 내용)을 잇는다면 VCS는 (이름, 작성 시각, 브랜치) → 내용으로 확장하여 ‘어느 게 최신인가’를 자동으로 정리해줍니다. 그런데 도구를 도입하는 것만으로는 끝나지 않습니다 — 그 위에 얹는 정책이 조직 확장성을 좌우합니다. 가장 중요한 정책은 진실 공급원을 단 하나로 고정하는 것입니다(원-버전 규칙 + 트렁크 기반 개발). 어느 버전을 쓸지 ‘선택’할 수 있는 순간, 확장성은 무너집니다.
쉽게 말하면
여러 명이 같은 발표 자료를 돌려가며 고치는 상황을 떠올려봅니다. 메일로 파일을 주고받다 보면 프레젠테이션 v5_최종_빨간줄_최팀장 버전 2.pptx 같은 괴상한 파일명이 쌓이고, 결국 “그래서 지금 진짜 최신이 뭐죠?”라는 질문 앞에서 모두가 멈춰 섭니다. 버전 관리가 푸는 문제가 바로 이 한 질문입니다.
해법의 절반은 도구입니다 — 누가 언제 뭘 고쳤는지 자동으로 추적하는 시스템(Git 같은)을 쓰면 됩니다. 하지만 진짜 절반은 규칙입니다. 아무리 좋은 도구를 써도, 팀원마다 “내 노트북에 있는 게 진짜야”라고 우기면 _최종_최종 지옥은 그대로 재현됩니다. 그래서 “이 폴더의 이 한 파일만이 진실이다, 나머지는 전부 작업 사본일 뿐이다”라고 단 하나를 못 박는 약속이 필요합니다.
이 장 전체는 결국 “버전 관리는 기술 절반, 정책 절반인데, 조직이 커질수록 정책이 승부를 가른다” 는 이야기입니다. 그 정책의 핵심은 단 하나 — ‘진실은 한 곳에만 있다’를 끝까지 지키는 것입니다.
왜 중요한가?
버전 관리는 프로그래밍과 소프트웨어 엔지니어링을 가르는 결정적 차이를 드러냅니다. 프로그래밍이 ‘즉각 이루어지는 소스 코드 생산’이라면, 소프트웨어 엔지니어링은 ‘제품을 장기간 지속 관리’하는 일입니다. 이 둘을 가르는 축이 바로 시간입니다. 프로그래밍에는 꼭 필요하지 않지만 소프트웨어 엔지니어링에서는 아주 중요한 요소가 시간이고, VCS는 그 시간을 코드 위에 명시적으로 올려놓는 도구입니다.
VCS가 없으면 어떻게 될까요? 가장 단순하게는 프로젝트를 통째로 복사해 주고받는 방식이 있습니다. 사무실 타임존이 다르거나 파일을 동시에 수정할 일이 없는 아주 작은 팀이라면 그럭저럭 굴러갑니다. 하지만 최신 버전이 헷갈릴 가능성이 조금이라도 생기는 순간, ‘최신 버전 추적’이라는 귀찮은 문제가 즉시 발생합니다. 공유 스토리지를 도입하면 한 단계 나아지지만, 여전히 서로의 작업을 덮어쓰지 않도록 협력해야 하고 누군가 빌드되지 않는 결과물을 올리면 팀 전체가 마비됩니다.
버전 관리는 ‘되돌리기’ 기능 때문에 중요한 게 아닙니다. 프로젝트 전체 되돌리기는 평생 한 번 쓸까 말까입니다. 진짜 가치는 개발자가 한 명이든 여럿이든 일관된 프로세스로 개발할 수 있다는 데 있습니다. 일관된 프로세스는 팀과 조직을 확장하는 열쇠입니다. 개발은 본질적으로 분기(branch)하고 병합(merge)하며 전진하는 과정이고, VCS는 ‘어느 게 최신이지?‘라는 질문을 사라지게 하여 여러 개발자와 여러 시점 사이를 교통정리해줍니다.
여기에 더해 VCS는 비기술적 측면에서도 행동양식을 바꿔놓습니다. 커밋 메시지를 작성하는 잠깐 동안 개발자는 “마지막 커밋 이후 내가 무엇을 이뤘는가?”를 되돌아봅니다. 이 작은 성찰의 순간, 그리고 정적 분석·테스트 커버리지를 점검하는 체크포인트가 코드 품질을 끌어올립니다.
핵심 내용
버전 관리의 본질 — 파일시스템에 ‘시간’을 더한 것
VCS는 파일의 시간에 따른 변경 기록(버전)을 추적하는 시스템입니다. 메타데이터와 버전별 복사본을 합쳐 리포지터리(repository, 줄여서 repo) 라고 부릅니다. VCS를 표준 파일시스템의 확장으로 보면 이해가 쉽습니다.
VCS = 시간을 입력으로 받는 파일시스템
파일시스템: (파일 이름) => 파일 내용 VCS: (파일 이름, 작성 시각, 브랜치) => 파일 내용따로 명시하지 않으면 브랜치는 기본적으로 메인 브랜치를 가리키며, 메인 브랜치를 보통
head,default,trunk등으로 부릅니다.
초창기 VCS는 파일 단위 락(lock) 으로 한 사람이 파일을 수정하는 동안 다른 사람의 수정을 막았습니다. 이후 진보한 VCS는 변경된 파일들의 묶음을 ‘하나의 단위’, 즉 논리적으로 ‘하나의 변경’으로 인식하여 원자적(atomic) 으로 처리하기 시작했습니다. 원자성이 보장되지 않으면(1990년대의 CVS가 그랬습니다) 커밋 시 충돌이 나서 변경 내역을 잃어버릴 수 있습니다. 원자성은 기존 변경이 의도치 않게 덮어써지는 사태를 막아줍니다.
이를 위해 개발자의 로컬 복사본에도 자체 메타데이터가 필요합니다. 이 축소된 복사본을 보통 클라이언트(client) 혹은 워크스페이스(workspace) 라고 합니다. VCS 설계에 따라 이 복사본 자체가 로컬 리포지터리가 되기도 하고(분산형), 단순히 메타데이터 일부만 관리하기도 합니다(중앙집중형).
중앙집중형 VCS vs 분산형 VCS
최신 VCS들은 핵심 기능(원자적 커밋)이 대동소이하고, 차이는 주로 사용자 경험·용어·성능에서 나타납니다. 그래서 ‘A가 B보다 낫다’는 논쟁은 대개 취향의 문제입니다. 하지만 아키텍처는 설정·정책·확장성을 크게 좌우하므로, 두 갈래의 구조적 차이를 아는 것은 중요합니다.
flowchart TB subgraph C["중앙집중형 VCS (Subversion 등)"] direction TB CR[("중앙 리포지터리<br/>(유일한 진실)")] D1["개발자 A<br/>워크스페이스"] -->|커밋은 반드시 중앙으로| CR D2["개발자 B<br/>워크스페이스"] -->|커밋은 반드시 중앙으로| CR CR -->|체크아웃| D1 CR -->|체크아웃| D2 end subgraph DV["분산형 VCS (Git, Mercurial)"] direction TB R1[("개발자 A<br/>로컬 리포 (완전판)")] R2[("개발자 B<br/>로컬 리포 (완전판)")] R1 <-->|push/pull| R2 R1 <-.->|push/pull| ORIGIN[("'중앙'<br/>= 정책상의 약속일 뿐")] R2 <-.->|push/pull| ORIGIN end
중앙집중형 VCS
중앙집중형 VCS(Centralized VCS) 는 단 하나의 중앙 리포지터리를 이용합니다. 개발자는 파일을 체크아웃해 로컬에서 작업하지만, 버전 관리 상태와 관련한 작업(추가·동기화·갱신)은 반드시 중앙 서버를 거쳐야 합니다. 1970~80년대 초의 RCS는 락킹에 집중했고, 1990~2000년대 초에는 락 대신 ‘어느 버전과 동기화했는지’를 추적하는 방향으로 진보했습니다. CVS는 RCS를 개량해 한 파일을 여러 개발자가 동시에 체크아웃할 수 있게 했고, Subversion(서브버전) 은 커밋과 버전 추적을 완벽히 원자적으로 수행하며 이 모델을 대표하는 시스템이 되었습니다.
분산형 VCS
2000년대 중반부터 분산형 VCS(DVCS, Distributed Version Control System) 가 주류가 되었습니다. 선봉이 Git 과 Mercurial(머큐리얼)입니다. 가장 큰 개념적 차이는 ‘어디에 커밋하는가?’ 혹은 ‘어느 복사본이 리포지터리로 간주되는가?’ 입니다. 복제본(clone, fork)을 가졌다면 그 자체로 커밋 가능한 완전한 리포지터리를 소유한 것이고, 변경 이력 같은 메타데이터까지 그 안에 오롯이 담겨 있습니다.
분산형 VCS에서 ‘중앙’은 기술이나 프로토콜이 아니라 순전히 정책의 문제로만 존재합니다. 이 모델은 오프라인 작업과 협업에 유리하며, 특히 오픈 소스 진영처럼 팀이 심하게 분산된 경우에 많이 채택됩니다. 현시점 가장 지배적인 VCS는 분산형인 Git입니다 — “어떤 VCS를 쓸지 고민된다면 Git을 쓰세요. 모두가 쓰고 있다면 그만한 가치가 있을 가능성이 큽니다.”
정작 구글은 중앙집중형을 씁니다
구글의 주 리포지터리는 자체 제작한 중앙집중형 VCS입니다. 5만 명의 엔지니어가 쏟아내는 수천 개의 커밋을 수용하려면 중앙집중형 방식과 클라우드 스토리지가 더 적합하다고 보기 때문입니다. 대부분의 기존 분산형 도구는 5만 명 규모의 이력·메타데이터 전송을 감당하도록 확장하기 어렵습니다. 조직이 커질수록 리포지터리끼리 주고받는 데이터 대부분이 사실상 낭비가 되기도 합니다.
진실 공급원(SSOT) — 정책의 심장
중앙집중형 VCS는 설계 단계에서부터 진실 공급원(source-of-truth) 개념을 내장합니다. 트렁크에 가장 최근 커밋된 것이 곧 현재 버전이고, 그 위에 변경을 다시 커밋하면 작업이 ‘완료(done)‘됩니다.
반면 분산형 VCS에는 여러 리포지터리 중 어느 것이 단일 진실 공급원(SSOT, Single Source of Truth) 이냐는 개념이 애초에 존재하지 않습니다. 이론상 중앙 통제 없이 커밋과 PR(pull request)을 전혀 다른 브랜치로 전달할 수 있습니다. 곧 _최종_빨간줄 세상으로 회귀할 위험이 있다는 뜻입니다. 그래서 분산형 VCS를 운영하려면 정책과 규범을 더 명확하게 정해 지켜야 합니다. 실제로 잘 관리되는 깃허브·깃랩(GitLab) 프로젝트는 특정 리포지터리의 특정 브랜치 하나를 진실 공급원으로 정하고, 변경이 그 트렁크에 반영되어야 비로소 ‘완료’로 칩니다.
진실 공급원이 없다면? — 결국 그것을 다시 발명하게 된다
팀이 분산형 철학에 심취해 어떤 브랜치도 진실 공급원으로 지정하지 않았다고 해봅시다.
- 릴리스 빌드에 지난 몇 주간 모든 기능을 담으려면, 누군가는 포함시킬 기능 목록을 따로 관리해야 합니다 — 이 목록이 곧 ‘중앙화된 진실 공급원’의 모방입니다.
- 새 팀원이 “정상 작동하는 최신 코드를 어디서 복사하나요?”라고 물으면 답할 수가 없습니다.
결론: 팀이 커져도 인원수 증가보다 적은 노력으로 관리하려면, 단 하나의 리포지터리와 하나의 브랜치를 궁극적 진실 공급원으로 지정해야 합니다.
다만 진실 공급원은 상대적입니다. 리눅스 커널이라면 구글·레드햇·리누스(Linus)가 각자 다른 진실 공급원을 쓰는 게 전혀 어색하지 않습니다. 분산형 VCS는 조직과 그 진실 공급원이 계층 구조일 때, 그리고 조직 외부에 공개되지 않을 때 특히 적합합니다 — 레드햇 엔지니어는 자사 리포에 수시로 커밋하고 상위(리누스)로부터 주기적으로 변경을 받아옵니다.
브랜치 관리 — ‘진행 중인 작업’도 결국 브랜치다
리비전 추적 능력 덕분에 다양한 관리 방식을 고민하게 되는데, 이를 통틀어 브랜치 관리(branch management) 라고 합니다. 출발점은 한 가지 인식입니다 — ‘진행 중인 작업(work-in-progress)‘은 모두 하나의 브랜치와 같습니다. 아직 커밋하지 않고 계류 중인 변경도, 브랜치에 커밋한 변경과 개념적으로 다르지 않습니다. 단지 검색하고 비교하기가 더 어려울 뿐입니다.
이 개념을 명시적으로 지원하는 시스템도 있습니다. Perforce는 모든 변경에 두 개의 리비전 번호를 부여합니다 — 하나는 변경이 만들어진 시점의 브랜치를, 다른 하나는 다시 커밋된 브랜치를 가리킵니다. 그래서 파일을 변경 중인 사람이 누구이고, 그 사람이 아직 커밋하지 않은 변경 내용이 무엇인지까지 검토할 수 있습니다(반면 Git은 브랜치를 따서 수정·병합하는 흐름으로, 커밋 전 작업은 로컬에 숨겨져 있습니다).
‘커밋 전 작업 = 하나의 브랜치’라는 생각은 특히 리팩터링에서 잘 들어맞습니다. Widget을 OldWidget으로 이름 바꾸는 작업은 조직의 정책에 따라 세 단계로 해석될 수 있습니다.
- 진실 공급원 트렁크 브랜치에 있는
Widget의 이름만 바꿉니다. - 진실 공급원 리포지터리의 모든 브랜치에서 이름을 바꿉니다.
- 모든 브랜치는 물론,
Widget을 참조하는 계류 중인(아직 커밋 안 된) 변경까지 다 찾아서 바꿉니다.
3번은 ‘편집하려고 파일을 연 엔지니어’까지 추적한다는 뜻인데, 상용 중앙집중형 VCS가 그 사람들을 추적하는 이유 중 하나입니다.
개발 브랜치 중독 — 미루기가 부르는 확장성 붕괴
일관된 단위 테스트가 보편화되기 전에는 작은 변경 하나도 시스템을 망가뜨릴 위험이 컸기에 ‘트렁크’를 특별 취급하는 게 당연했습니다. 그래서 개발 브랜치(dev branch) — ‘구현은 다 했지만 커밋하진 않았어요’와 ‘이제부터 이 코드를 기준으로 개발하세요’의 중간 단계 — 를 기능별로 따로 만들어 쓰곤 했습니다.
문제는 이 방식이 중독적으로 악화된다는 점입니다.
flowchart TD A["오래된 개발 브랜치를 병합하니<br/>안정성이 떨어진다"] --> B["'병합은 위험하다'는 결론"] B --> C["정면돌파 대신 병합을 최대한 미룸"] C --> D["미병합 브랜치 위에서<br/>또 다른 브랜치를 땀"] D --> E["브랜치 수 증가 →<br/>병합 난이도 치솟음"] E --> F["빌드 마스터·병합 관리자 같은 직책 등장<br/>'주간 병합 전략 회의'"] F --> A
거대한 개발 브랜치를 병합할 때는 너무 많은 것이 한꺼번에 바뀐 상태라, 문제의 원인 범위를 좁히기가 어렵습니다. 그래서 큰 단위로 한 번에 병합하기보다 작게 자주 병합하는 게 쉽습니다. 변경을 작성한 당사자가 직접 병합하는 편이, 관련 없는 변경들을 나중에 누군가 몰아서 병합하는 것보다 쉽기 때문입니다.
해법은 트렁크 기반 개발(trunk-based development) 입니다. 테스트와 CI를 적극 활용해 모든 빌드·테스트가 항상 성공하도록 관리하고, 완벽하지 않거나 테스트되지 않은 기능은 (브랜치로 격리하는 대신) 비활성화합니다. 엔지니어 개개인이 트렁크와 동기화하고 트렁크에 커밋합니다. 그러면 ‘병합 전략’ 회의도, 값비싼 대규모 병합도, “어떤 버전의 라이브러리를 쓸까”를 두고 벌이는 토론도 사라집니다 — 버전이 하나뿐이니까요.
릴리스 브랜치 — 개발 브랜치와 달리 무해하다
제품의 릴리스 간격(혹은 수명)이 몇 시간 이상이면 릴리스 브랜치를 따로 생성하는 게 좋습니다. 이 브랜치는 릴리스한 제품과 정확히 같은 코드를 담습니다. 공식 릴리스 후 심각한 결함이 발견되면, 트렁크에서 해당 수정 코드를 최소한으로 선별(cherry-pick) 하여 릴리스 브랜치로 병합합니다.
개발 브랜치와 달리 릴리스 브랜치는 대체로 무해합니다. 가장 큰 차이는 생을 마감하는 모습입니다 — 개발 브랜치는 트렁크에 다시 병합되며 중간에 또 다른 브랜치를 파생시키지만, 릴리스 브랜치는 홀로 존재하다 결국 사라집니다.
최고 수준 조직은 릴리스 브랜치조차 없다
구글의 DORA(DevOps Research and Assessment) 연구에 따르면, 지속적 배포(CD, continuous deployment)가 잘 자리 잡은 조직은 대체로 릴리스 브랜치를 건너뜁니다 — 수정사항을 적용해 다시 배포하는 게 훨씬 쉬우니, 별도 브랜치를 두고 병합할 코드를 선별하는 일이 모두 불필요한 오버헤드이기 때문입니다. 단, 고객에게 실물 기기를 판매하는 조직은 배포된 정확한 버전을 알아야 하므로 릴리스 브랜치가 적합합니다. 같은 연구는 ‘트렁크 기반 개발 + 장수 개발 브랜치가 적은’ 조직일수록 기술적 성취가 뛰어나다고 말합니다.
원-버전 규칙 — 선택지를 없애는 것이 곧 확장성
구글의 버전 관리 정책 중심에는 원-버전(One-Version) 이 있습니다. 단일 진실 공급원 개념을 확장한 것으로, 핵심은 이렇습니다.
원-버전 규칙(One-Version Rule)
개발자가 ‘이 구성요소는 어떤 버전을 사용해야 하죠?‘라고 묻는 상황을 만들지 않아야 합니다.
구체적으로는 ‘모든 의존성이 우리 리포지터리에 담겨 있고, 각 의존성은 단 하나의 안정된 버전만 존재해야 한다’는 뜻입니다. 진실 공급원이 단일 리포지터리·단일 브랜치·단일 카피로 구성된다는 정책입니다.
이게 왜 그토록 중요한지는 여러 버전을 허용했을 때 벌어지는 일을 보면 분명해집니다. 어떤 팀이 공통 인프라(예: Abseil) 코드에서 버그를 발견하고, 원본을 고치는 대신 포크하여 Abseil'로 자체 해결한 뒤 이름·심볼을 그대로 둔 채 주변 팀에 “개선 버전 쓰세요”라고 알렸다고 합시다. 코드베이스에 갈림길이 하나 생긴 것입니다.
flowchart BT Abseil["Abseil (원본)"] AbseilF["Abseil' (포크)"] liba["liba"] -.->|의존| Abseil libb["libb"] -->|의존| AbseilF libc["libc"] -->|의존| AbseilF PA["프로젝트 A"] --> liba PA --> libb PA --> libc
프로젝트 A는 liba를 통해 원본 Abseil에, libb·libc를 통해 포크된 Abseil'에 동시에 의존하게 됩니다. 전이 의존성(transitive dependency) 에 의해 같은 라이브러리의 서로 다른 두 버전에 모두 묶이는 것입니다. 운이 좋으면 빌드 실패로 끝나지만, 최악의 경우 두 버전이 함께 링크되어 이해하기 어려운 런타임 버그(다이아몬드 종속성) 가 발생합니다. 같은 대상을 가리키는 전이 의존성은 반드시 단 하나를 가리켜야 합니다.
셰이딩 같은 트릭은 미봉책일 뿐
자바 업계의 셰이딩(shading) 은 라이브러리 내부 의존성(클래스·함수)의 이름을 바꿔 충돌을 피하는 기술입니다. 함수 수준에서는 기술적으로 잘 동작하지만, 패키지를 넘나드는 타입(type) 이 대상이면 이론적으로도 현실적으로도 잘 작동하지 않습니다. 분리 컴파일·링커 숨기기도 마찬가지로, 여러 버전 공존이라는 근본 문제에 천을 덧대는 미봉책에 불과합니다.
장수 브랜치는 (웬만하면) 금지
원-버전 규칙에는 한 가지 정책이 내포되어 있습니다 — 개발 브랜치를 되도록 만들지 말고, 만들더라도 매우 짧게 쓰고 없애라. 애자일 프로세스부터 DORA 연구, 피닉스 프로젝트까지 지난 20년간 업계가 공통으로 얻은 교훈이 ‘진행 중인 작업을 줄여라’였습니다.
신버전을 별도 브랜치에서만 제공하면 왜 문제일까요? 인프라팀이 Widget의 개선 버전을 만들었다고 합시다. 신버전을 별도 개발 브랜치에만 두고 일부 프로젝트가 쓰기 시작하면, 코드베이스에 ‘두 가지 버전 중 선택’이라는 갈림길이 다시 생깁니다.
flowchart LR subgraph BAD["❌ 좋지 않은 정책"] direction TB T1["트렁크: Widget"] DB["개발 브랜치: Widget'<br/>(별도 브랜치에서만 제공)"] end subgraph GOOD["✅ 권장 정책"] direction TB T2["트렁크: Widget + Widget'<br/>(Widget'은 준비 완료까지<br/>비활성화 · 다른 프로젝트에서 안 보이게 숨김)"] end BAD ==>|원-버전 준수| GOOD
제대로 하려면 신버전 Widget' 역시 트렁크에 커밋하되, 준비될 때까지 실행 파일에 포함되지 않도록 비활성화하고, 가시성을 설정해 다른 개발자들이 보지 못하게 숨겨야 합니다. 구글에서 모노리포로 작업하는 팀은 약 1,000개인데, 장수 개발 브랜치에서 작업하는 팀은 두어 개뿐입니다. 그런 팀은 대개 ‘버전 간 호환성 요구사항이 특이하다’거나 ‘API 호환성을 이미 약속해 구버전·신버전을 함께 운영해야 한다’는 명확하고 드문 이유를 가집니다.
빌드 호라이즌(build horizon)
잠재적 버전 왜곡이 지속되는 기간에 상한선을 두는 정책입니다. 프로덕션에서 구동 중인 모든 제품은 최대 6개월 안에 다시 빌드하여 재배포해야 합니다(보통은 훨씬 자주 이루어집니다). 장수 브랜치가 슬그머니 영생하는 것을 막는 안전장치입니다.
모노리포 — 수단일 뿐, 목적이 아니다
구글의 소스 코드 대부분은 하나의 리포지터리, 즉 모노리포 에서 관리됩니다(2016년 모노리포 논문으로 공개되었습니다). 자체 제작한 중앙집중형 VCS인 Piper(파이퍼) 가 80TB가 넘는 콘텐츠를 담으며, 하루 6~7만 건의 커밋을 받아냅니다. 모노리포의 가장 큰 이점은 원-버전을 고수하기가 쉽다는 것입니다 — 공식 버전이나 중심 리포지터리를 찾는 과정이 불필요하고, 도구·코드 최적화의 혜택이 조직 전체에 빠르게 전파됩니다.
모노리포가 정답은 아니다 — 중요한 건 '원-버전'이다
모노리포는 원-버전을 지키기 쉽게 해주는 수단이지 그 자체가 목적이 아닙니다. 조율과 동기화 없이 무수한 리포지터리를 만드는 멀티리포 방식도 잘 작동합니다(리눅스 배포판이 그 예입니다). 핵심은 모노리포냐 아니냐가 아니라, ‘원-버전’ 원칙을 최대한 준수하느냐입니다. 즉 이미 쓰는 라이브러리에 의존성을 추가할 때 개발자가 버전을 ‘선택’하게 해서는 안 됩니다.
멀티리포를 쓰더라도 이 경험을 흉내 낼 수 있습니다. Git의 서브모듈, Bazel의 외부 의존성, CMake의 서브프로젝트는 모노리포 유지 비용 없이 모노리포처럼 작업하게 해줍니다. 여러 리포지터리를 묶어 가상 모노리포(VMR, virtual monorepo) 형태로 쓰면, 원-버전을 유지하면서도 실험적·대외비 프로젝트는 더 쉽게 분리할 수 있습니다. 개인정보·법 준수·비밀·보안 요구사항이 모든 프로젝트에 동일하게 적용되는 조직이라면 진정한 모노리포가 좋고, 그렇지 않다면 가상 모노리포가 두 방식의 장점을 균형 있게 취하는 길입니다.
Quote
결국은 어떤 파일시스템을 선택하느냐보다 그 안에 무엇을 담느냐가 더 중요합니다.
비교 / 트레이드오프
중앙집중형 VCS vs 분산형 VCS
| 관점 | 중앙집중형 VCS | 분산형 VCS |
|---|---|---|
| 리포지터리 | 단 하나의 중앙 리포 | 모든 복제본이 완전한 리포 |
| 커밋 대상 | 반드시 중앙 서버 | 로컬 후 원하는 곳으로 push |
| 진실 공급원 | 설계에 내장 (트렁크) | 정책으로만 존재, 직접 지정 필요 |
| 오프라인 작업 | 제약 큼 | 자유로움 |
| 대표 제품 | Subversion, Perforce, Piper(구글) | Git, Mercurial |
| 잘 맞는 상황 | 초대형 단일 조직, 일관된 정책 | 분산된 팀, 계층적 조직, 오픈 소스 |
개발 브랜치 vs 릴리스 브랜치
| 관점 | 개발 브랜치 | 릴리스 브랜치 |
|---|---|---|
| 목적 | 미완성 기능 격리 | 출시된 버전 동결 |
| 생의 마감 | 트렁크에 다시 병합 | 홀로 존재하다 소멸 |
| 위험도 | 높음 (장수화 시 병합 지옥) | 낮음 (대체로 무해) |
| 권장 여부 | 웬만하면 금지, 비활성화로 대체 | 릴리스 수명 길면 합리적 |
| 트렁크와의 관계 | 동기화 안 하면 점점 멀어짐 | cherry-pick으로 최소 수정만 받음 |
모노리포 vs 멀티리포
진짜 쟁점은 두 방식의 우열이 아니라 ‘원-버전을 지킬 수 있느냐’ 입니다. 모노리포는 원-버전을 거저 얻게 해주지만, 모든 프로젝트에 동일한 보안·법규 정책을 적용할 수 있어야 합니다. 멀티리포는 리포지터리별로 접근 권한·가시성을 다르게 가져가기 쉽지만, 원-버전을 지키려면 서브모듈·가상 모노리포 같은 도구로 ‘리포지터리 간 의존성’과 ‘하나의 가상 트렁크’를 별도로 구축해야 합니다.
내 생각
-
‘어느 게 최신이지?‘를 없애는 것 = 백엔드에서 환경별 설정 드리프트를 없애는 것과 같은 문제. dev/staging/prod의 설정·스키마·라이브러리 버전이 제각각이면 그게 곧
_최종_빨간줄지옥의 인프라 버전입니다. IaC로 단일 소스를 강제하는 이유가 원-버전 규칙과 정확히 같습니다. -
다이아몬드 종속성은 자바 백엔드의 일상. Spring Boot 의존성 트리에서
mvn dependency:tree를 찍으면 같은 라이브러리가 두 버전으로 끌려오는 일이 흔합니다. BOM(Bill of Materials)으로 버전을 한 곳에 고정하는 게 바로 원-버전 규칙의 실무 구현입니다 — “각 의존성은 단 하나의 버전만”이 BOM 한 줄로 강제됩니다. -
트렁크 기반 개발의 전제는 ‘feature flag’와 ‘CI’. 미완성 기능을 트렁크에 커밋하되 비활성화하라는 게 곧 피처 플래그입니다. 플래그·강력한 CI 없이 트렁크 기반만 흉내 내면 트렁크가 깨진 채 방치됩니다. 도구가 정책을 떠받쳐야 한다는 게 이 장의 일관된 메시지입니다.
-
API 버저닝은 ‘장수 브랜치 금지’의 예외 케이스. 외부 클라이언트에 호환성을 약속한 순간 구버전·신버전을 함께 굴려야 하므로, 빌드 호라이즌처럼 ‘v1 EOL 날짜’를 못 박아 장수화를 막는 게 현실적 절충입니다.
관련 개념
- Ch15 폐기 — 원-버전 규칙을 지키려면 결국 구버전을 폐기시켜야 합니다. 셰이딩·포크가 쌓이는 것을 막는 퇴행 방지가 이 장의 ‘선택지 없애기’와 직결됩니다
- Ch11 테스트 개요 · Ch12 단위 테스트 — 트렁크 기반 개발을 떠받치는 토대. 일관된 테스트가 없으면 개발 브랜치 격리로 회귀합니다
- Ch01 소프트웨어 엔지니어링이란 — ‘프로그래밍 + 시간 = 소프트웨어 엔지니어링’이라는 정의가 VCS를 필수 도구로 만드는 근거입니다