지난 달에 있던 Hacktoberfest에 참여하면서 Gatsby에 기여를 하게 되었습니다. 단일 리포지터리로 된 프로젝트는 처음 경험해서 코드가 반영되고 갱신되는 과정이 흥미로웠습니다. 단일 리포지터리를 사용하면 다중 리포지터리에 비해 어떤 장점이 있는지 여러 글을 읽어보게 되었는데 그 중 하나를 번역하게 되었습니다.

이 글은 Dan LuuAdvantages of monorepos 번역입니다.


단일 리포지터리(monorepo)의 좋은 점

다음 같은 대화를 자주 합니다.

누군가: 페이스북/구글에서 거대한 단일 리포지터리를 사용한다는 얘기 들었어? 뭐임 대체!

: 그래, 엄청 편하겠다. 그럴 것 같지 않아?

누군가: 엥 내가 들어본 얘기 중에 가장 황당한 얘기다. 페이스북이나 구글은 단일 리포지터리에 모든 코드를 넣는 게 얼마나 끔찍한 아이디어인지 모르는 건가?

: 내 생각에는 페이스북, 구글에서 일하는 엔지니어도 작은 크기 리포지터리가 익숙할 거야. (Junio Hamano도 구글에서 일하지 않나?) 그리고 여전히 단일 거대 리포지터리를 선호하는데 거기에는 이런 [이유]가 있어.

누군가: 흠, 그래, 꽤 괜찮은 것 같네. 내 생각엔 여전히 이상하긴 하지만 왜 이런 리포지터리를 원하는지도 이해할 수 있을 것 같다.

"[이유]"는 생각보다 꽤 깁니다. 그러니 같은 대화를 계속 반복하는 것보다 글로 작성해서 이유를 설명하려고 합니다.

단순화된 편성

다중 리포지터리를 사용한다면 일반적으로 프로젝트 당 리포지터리가 있거나 연관된 프로젝트를 기준으로 리포지터리가 있을 겁니다. 하지만 "프로젝트"가 무엇인가에 대한 정의는 어떤 팀 또는 회사에 있는가에 따라 달라질 겁니다. 이런 이유로 리포지터리를 합치거나 분리하게 되는데 이런 작업은 순수하게 부가적인 비용이 됩니다. 어떤 경우에 프로젝트를 분리하게 되는지 예를 들면 프로젝트가 너무 큰 경우, 또는 버전 관리 도구의 이력이 너무 많아 최적화가 필요한 경우에 프로젝트를 분리하게 됩니다.

단일 리포지터리를 사용하면 버전 관리 도구가 특정 방식으로 구조를 구성하도록 하는 것이 아니라 논리적으로 가장 일관적인 방법으로 프로젝트를 편성하고 묶을 수 있습니다. 또한 단일 리포지터리를 사용하면 의존성을 관리하는 부하를 줄일 수 있습니다.

편성 단순화의 부수 효과는 프로젝트 간 탐색을 더 쉽게 할 수 있다는 점입니다. 단일 리포지터리를 사용하면 기본적으로 파일 시스템에서 파일을 탐색한다고 말하는 것과 동일하게 모든 프로젝트를 탐색할 수 있습니다. 다중 리포지터리로 구성하면 대부분 두 계층으로 분리된 탐색이 필요합니다. 먼저 프로젝트 안을 탐색할 때는 파일 시스템에서 말하는 탐색이 필요하고, 메타 계층으로서 프로젝트 사이를 찾는 탐색도 필요합니다.

단일 리포지터리를 사용하면 나타나는 이 부수 효과에는 또 부수 효과가 있는데 아주 쉽게 개발 환경을 꾸리고 빌드, 테스트까지 구동할 수 있다는 점입니다. 다른 프로젝트를 탐색하기 위해서 cd를 사용할 수 있고 cd; make로도 당연히 가능하게 됩니다. 이런 구조가 동작하지 않는 것은 당연히 이상하게 보일 정도로 제대로 동작합니다. 동작하기 위해 필요한 어떤 툴링 작업이든 문제없이 완료됩니다.1 물론 기술적으로 다중 리포지터리를 사용하더라도 동일한 작업을 할 수 있지만 자연스럽지는 않습니다. 즉, 생각처럼 동작하지 않을 때가 종종 있다는 의미입니다.

단순화된 의존성

아마 이 사실은 말할 필요도 없겠지만 다중 리포지터리를 사용하면 서로 어떤 버전에 의존하고 있는지 정의하는 방법이 필요할 것입니다. 듣기에는 복잡하지 않은 문제라고 느낄지 몰라도 실무에서는 대부분의 해결책이 성가신 데다 많은 부하가 따릅니다.

단일 리포지터리를 사용하면 모든 프로젝트를 위한 단일 버전을 쉽게 만들 수 있습니다. 여러 프로젝트에 걸쳐서 원자적인 커밋(atomic cross-project commit)이 가능하기 때문에 리포지터리는 언제나 일관적인 상태를 유지할 수 있습니다. 즉, #X 커밋에 모든 프로젝트 빌드가 동작해야 합니다. 빌드 시스템에서는 여전히 의존성을 지정할 필요가 있지만 Makefile이나 bazel BUILD 파일을 사용할 때는 다른 것과 같이 버전 관리 내에서 확인할 수 있습니다. 그리고 단 하나의 버전 번호를 갖게 되는 것으로 Makefile이나 BUILD 파일, 또는 어떤 빌드 방법을 사용하더라도 특정 버전 번호를 지정할 필요가 없게 됩니다.

툴링(Tooling)

탐색과 의존성을 단순하게 하면 도구를 만드는 일도 훨씬 쉬워집니다. 도구를 만들며 각 리포지터리의 관계를 이해하도록 하는 방법 대신에 모든 파일이 리포지터리 내에 들어 있기 때문에 이런 도구는 (리포지터리 내에 의존성을 정의한 몇 파일을 포함해서) 그저 파일을 읽는 일만 하면 됩니다.

이 부분은 사소한 것처럼 느껴질 수 있겠지만 Christopher Van Arsdale의 예에서 빌드가 얼마나 단순하게 가능한지 볼 수 있습니다.

구글 내부의 빌드 시스템은 대형 모듈러 블록 코드를 사용해서 빌드를 환상적으로 쉽게 수행합니다. 크롤러가 필요해요? 여기에 몇 줄 추가합니다. RSS 파서가 필요해요? 몇 줄을 더 추가합니다. 대형 분산, 장애 허용 데이터 저장소? 물론 몇 줄을 더 추가합니다. 이렇게 만들어진 코드 블록과 서비스는 여러 프로젝트에서 공유되며 쉽게 통합할 수 있습니다. … 이처럼 레고(LEGO)처럼 조립 가능한 개발 프로세스는 오픈소스 세계에서 자주 일어나지 않습니다. … 이런 다양한 상태의 결과로 (추측하건대) 오픈소스에 복잡한 장벽이 생겨나고 지난 몇 년간 큰 변화가 없었습니다. 이런 이유에서 구글과 같이 쉽게 관리하는 경우와 그러지 못한 여러 오픈소스 프로젝트의 차이를 만들게 되었습니다.

Arsdale이 편하다고 말한 이 시스템은 오픈소스가 되기 전에 전 구글 엔지니어가 페이스북, 트위터에서도 동일한 혜택을 누리기 위해서 bazel의 자체 버전을 작성했습니다.

이론적으로는 단순히 단일 리포지터리 없이도 모든 의존성을 해결하고 빌드를 수행하는 빌드 시스템을 만들어 낼 수 있습니다. 하지만 더 큰 노력이 필요한 데다 이런 노력에도 불구하고 문제없이 돌아가는 시스템을 본 적이 없습니다. Maven과 sbt는 그런 점에서 꽤 잘 만들어지긴 했지만, 버전 의존성 문제를 추적하고 고치는데 많은 시간을 들이는 것 또한 일반적으로 많이 겪게 됩니다. rbenv나 virtualenv와 같은 시스템은 문제를 우회하려는 노력이지만 개발 환경을 급증하게 하는 결과를 만들었습니다. 단일 리포지터리에서 HEAD는 항상 일관적이고 유효한 버전을 가리키고 있으며 다중 리포지터리의 버전을 추적하는 노력을 완전히 제거하게 됩니다2.

단일 리포지터리를 운영하면서 빌드 시스템에만 이득이 있는 것이 아닙니다. 더 예를 들자면 프로젝트 경계를 넘어서 정적 분석을 수행하는 일에도 추가적인 노력이 필요하지 않습니다. 프로젝트 간의 통합 테스트나 코드 검색과 같이 많은 부분에서도 훨씬 단순해집니다.

프로젝트 간 변경

많은 리포지터리를 갖고 있다면 프로젝트 간 변경은 고통스럽습니다. 일반적으로 각각의 리포지터리를 걸쳐 정합성을 맞추기 위해 어마어마한 양의 수작업이 따르거나 또는 핵과 같은 스크립트를 작성해서 돌려야 합니다. 스크립트가 동작하더라도 프로젝트 간 버전 의존성을 올바르게 고치기 위한 부하도 발생합니다. 10개의 내부 프로젝트에 걸쳐 사용하고 있는 API를 리팩토링한다면 아마 하루종일 시간 쓰기 좋은 분량일 것입니다. 수천 개의 내부 프로젝트에서 사용하고 있는 API를 리팩토링하는 경우라면 꿈도 희망도 없습니다.

단일 리포지터리라면 API와 모든 호출자의 리팩토링은 커밋 하나로 해결할 수 있습니다. 항상 이런 사소한 작업이 있는 것은 아니지만 수많은 작은 리포지터리를 수정하는 방법보다 훨씬 쉽습니다.

대부분은 CVS, RCS, ClearCase와 같은 버전 관리 도구를 사용하는 일이 불합리하다고 생각하고 있습니다. 이런 버전 관리 도구는 여러 파일에 걸친 원자적 커밋을 할 수 없습니다. 그래서 커밋의 타임 스탬프와 커밋 메시지, 또는 "진짜" 원자적으로 커밋된 파일이 어떤 것인지 확인할 수 있는 메타 정보를 확인해야 합니다. SVN, hg, git 등과 같은 도구는 여러 파일 변경도 하나의 커밋에 넣을 수 있기 때문에 이 문제를 해결할 수 있습니다. 단일 리포지터리는 동일한 문제를 여러 프로젝트에 걸쳐 해결할 수 있습니다.

이 접근 방법의 유용성은 단순히 대규모의 API 리펙토링에만 국한되지 않습니다. David Turner는 트위터에서 여러 리포지터리를 단일 리포지터리로 옮기는 작업을 했습니다. 이 작업은 작은 일이지만 여러 프로젝트에 걸친 변경과 이 변경을 배포하는 과정에서 어떤 부하가 발생하는지 이야기합니다.

[프로젝트 A]를 갱신해야 합니다. 하지만 이 작업을 위해서는 내 동료가 이 프로젝트의 의존성인 [프로젝트 B]에서 고친 코드가 필요합니다. 이 동료는 [프로젝트 C]에서 변경된 내용이 필요합니다. 만약 A를 고쳐서 배포하려 한다고 해도 C의 배포를 기다려야 하고 B 배포도 기다려야 하는 상황입니다. 하지만 모든 것이 하나의 리포지터리에 있다면 내 동료가 코드를 변경해서 커밋 하자마자 내 코드 변경도 즉시 가능합니다.

물론 git 버전에 의해 모든 것이 연결되어 있다면 다중 리포지터리에서도 가능할 겁니다. 하지만 내 동료는 두 개의 커밋이 필요할지도 모릅니다. 이런 작업에는 버전을 하나 집어서 "안정화" (더 움직이지 않도록) 하고 싶은 욕망이 항상 생깁니다. 하나의 프로젝트라면 문제가 없겠지만 상호의존적인 복잡한 프로젝트에서는 이조차도 그다지 쉽지 않은 방법입니다.

[다른 방향에서 본다면] 의존하는 부분 이 강제로 갱신돼야 하는 상황인데 이 또한 단일 리포지터리의 장점이라고 할 수 있습니다.

이 접근 방법은 단순히 프로젝트 간 변경을 쉽게 수행할 수 있는 것만 아니라 변경을 추적하는 일도 쉽게 만듭니다. git bisect을 여러 리포지터리에서 수행한다면 분명 다른 도구를 이용해서 메타 정보를 읽는 방법을 배워야 할 것이고 대부분 프로젝트에서는 그냥 간단하게 이런 작업을 하지 않아버리고 맙니다. 만약 이런 일을 한다고 해도 하나의 도구만 써도 충분한 작업을 여러 도구에 걸쳐서 하게 될 겁니다.

Mercurial과 git은 멋집니다! 실화입니다

CVS 또는 SVN에서 git 또는 hg로 변경한 경우에 가장 일반적으로 들을 수 있는 이야기는 엄청나게 생산성이 좋아졌다는 얘기입니다. 이 점은 사실입니다. 하지만 대부분 이런 응답의 관점은 git과 hg가 더 발달한 여러 측면(예를 들면 더 나은 코드 머지를 지원한다던가)이 그 이유였지 작은 리포지터리가 더 낫기 때문인 것은 아니었습니다.

사실 트위터는 git을, 페이스북은 Mercurial을 대형 단일 리포지터리를 지원하기 위해 고쳐서 사용하고 있습니다.

단점

물론 단일 리포지터리에도 단점이 있습니다. 여기서는 단점을 적지 않을 생각인데 이미 충분히 많은 논의가 있었기 때문입니다. 단일 리포지터리는 다중 리포지터리에 비해 엄격하게 우월한 지위를 갖는 것은 아닙니다. 그렇다고 엄격하게 나쁜 것도 아닙니다. 제 관점에서는 누구든 꼭 단일 리포지터리로 옮겨야 한다고 얘기하지 않습니다. 단지 단일 리포지터리를 쓰는 일이 완전히 부당하지 않다는 얘기를 하고 싶었습니다. 구글, 페이스북, 트위터, 디지털오션과 엣시(Etsy)에서 수백, 수천, 수만 개의 작은 리포지터리를 운영하는 대신 단일 리포지터리 사용을 선호하는 점에는 분명 좋은 이유가 있어서 그럴 겁니다.

다른 논의

광범위한 토론에 함께 한 Kamal Marhubi, David Turner와 Leah Hanson에게 감사 말씀 전합니다. 이 토론에서 적어도 절반 이상의 아이디어가 나왔습니다. 또한 이 글에서 오타와 실수를 찾아 준 Leah Hanson, Mindy Preston, Chris Ball, Daniel Espeset, Joe Wilder, Nicolas Grilly, Giovanni Gherdovich, Paul Hammant, Simon Thulbourn에게 감사드립니다.

Footnotes

  1. 이 부분은 제가 일했던 하드웨어 회사에서도 맞는 말입니다. 저는 이 회사에서 NFS에서 동작하는 RCS를 버저닝하는 단일 리포지터리를 만들어서 사용했습니다. 물론 사람들이 중앙 리포지터리에서 파일을 수정할 수 있게 할 수 없으니 억지로 이 작업을 가능하게 누군가가 여러 스크립트를 작성했습니다. 이런 시스템을 추천하진 않습니다만 엄청나게 단일 리포지터리처럼 해킹해서 쓰더라도 단일 리포지터리의 장점을 누릴 수 있습니다.

  2. 적어도 최신 의존성을 관리하는 메커니즘이 있어야 할 것입니다. 물론 이런 작업은 구글에서 문제가 없는데 구글은 코드를 작성해도 많은 수의 직원이 그 코드에 의존하기 때문에 모든 외부 의존성을 단일 리포지터리에 넣어도 전체 직원 규모에서 보면 오히려 비용이 절감되기 때문입니다. 제가 보기에 작은 회사에서 이런 접근 방법에 이득을 얻기에는 너무 비용이 많이 든다고 생각합니다.

색상을 바꿔요

눈에 편한 색상을 골라보세요 :)

Darkreader 플러그인으로 선택한 색상이 제대로 표시되지 않을 수 있습니다.