되도록 최신 버전을 사용하는게 왜 이렇게 어려울까?

해법 자체는 한 문단이면 말할 수 있습니다. 저희의 규칙은 놀랄 만큼 단순합니다 — 되도록 최신을 쓴다. 개별 레포가 "어느 버전을 박아둘까"를 고르는 대신, group name에 소속되면 그 family의 최신(검증된) 버전이 기본값으로 따라옵니다. 같은 family는 같은 버전으로 함께 움직이고, 아티팩트는 위에서 아래로만 흐릅니다. 그래서 "이 라이브러리 어느 버전 써요?"라는 질문은, 고를 선택지 자체가 없어지면서 대부분 사라집니다. 이걸 저희는 사내 Gradle 플러그인 — 편의상 version family plugin이라 부르는 — 으로 구현했습니다.
여기까지 들으면 "그거 우리도 group name에 규칙 붙이면 되겠네" 싶을 수 있습니다. 그런데 폴리레포를 오래 굴려본 분이라면, 바로 그 지점에서 손이 멈출 겁니다. 이런 걸 시도해봤고, 대부분 절반에서 흐지부지되는 걸 봤을 테니까요. 솔직히 말하면, 저희에겐 이 규칙이 그리 어렵지 않습니다 — 이미 그 아래에 필요한 조건이 갖춰져 있으니까요. 어려운 건 규칙이 아니라 그 규칙이 설 수 있는 땅을 갖추는 일이고, 바로 그 땅이 대부분의 조직에는 없습니다. 그래서 이 이야기의 무게는 "어떻게 만들었나"가 아니라 "왜 대부분의 조직에선 이 단순한 규칙조차 서지 못하는가"에 있습니다. 그 땅을 세우려 할 때 마주치는 세 가지 문제를, 그게 왜 어려운 지와 함께 이야기하려 합니다.
그 전에 문제를 한 겹만 정확히 해두겠습니다. 진짜 고통은 "어느 버전 써요?"라는 질문이 반복되는 게 아닙니다. 어느 버전이 "지금 써도 되는" 버전인지 답해줄 권위 있는 출처가 어디에도 없다는 것입니다. 최신 태그가 배포 가능한 건지 아직 검증 전인지, 이 모듈 버전과 저 모듈 버전이 같이 올라간 조합인지, 소비자가 Renovate PR을 그대로 머지해도 되는지 — 이 판단의 근거가 레포마다, 태그마다, 슬랙 스레드마다 흩어져 있습니다. 봇은 PR을 만들어 주지만, 그 조직의 검증 상태까지 알지는 못합니다. 이 흩어진 권위를 한 곳으로 모으는 것 — 그게 "되도록 최신을 쓴다"가 성립하기 위한 전제였고, 그 전제를 세우는 과정에서 세 가지 문제가 왜 어려운지를 배웠습니다.

§1. 첫 번째 문제 — 닭이 먼저인가 달걀이 먼저인가
version family가 작동하려면 전제가 하나 필요합니다. group name 하나로 버전·릴리스·태깅·게시 규칙을 전부 유도하려면, 그 규칙을 적용받는 모든 레포가 같은 빌드 시스템 위에 있어야 합니다. 어떤 레포는 Gradle, 어떤 레포는 Maven, 어떤 레포는 자체 스크립트라면, 정책을 해석하고 강제할 단일 지점이 없습니다. 플러그인은 자신이 붙을 수 있는 빌드 시스템에서만 규칙을 부여하고, 그 바깥은 제각각으로 남습니다.
문제는 여기서 순환이 생긴다는 것입니다. 버전 규칙을 강제하려면 동형성(전 제품이 같은 빌드 전략을 공유하는 상태)이 먼저 있어야 하는데, 그 동형성 자체가 이런 강제 장치 없이는 유지되지 않습니다. 통일돼 있어야 통일을 강제할 수 있고, 강제할 수단이 있어야 통일이 유지됩니다. 닭과 달걀입니다.
그린필드라면 이 순환은 문제가 아닙니다. 처음부터 한 방식으로 시작하면 되니까요. 하지만 대부분의 조직은 그린필드가 아닙니다. 그리고 그 이질성은 대개 잘못이 아니라 성장의 흔적입니다 — 빠르게 크던 시기에 각 팀에 "일단 너희가 편한 방식으로 빠르게 가라"며 자율성을 부여했고, 그 자율성이 팀마다 다른 빌드 방식, 다른 버전 관례, 다른 게시 규칙으로 굳었습니다. 속도를 위해 옳았던 결정이 규모가 커진 뒤 부채로 청구되는 것이죠. 이런 곳에서 "전 제품이 같은 빌드 전략을 공유한다"는 상태는 이미 도달하기 어려운 목표입니다. 여기가 대부분의 시도가 죽는 자리입니다 — 빌드가 제각각인 곳에 family를 얹으면 강제할 지점이 없어 일부 레포에만 걸치고, 반쪽짜리 규칙은 이내 흐지부지됩니다.

일반적인 회사가 여기서 막히는 건 게을러서가 아닙니다. 순환의 어느 고리를 먼저 끊을 것인가 — 이게 플러그인을 짜는 일보다 훨씬 어려운 진짜 문제입니다. version family가 가능했던 진짜 조건은 플러그인의 영리함이 아니라, 그 앞에 이미 이 순환을 끊어 놓은 동형성이 있었다는 것입니다. 도구는 그 위에 세운 한 층일 뿐입니다.
§2. 두 번째 문제 — 한번 세우면 되돌릴 수 없다
version family가 제대로 굴러가려면 강력한 전제가 하나 더 필요합니다. 내부 아티팩트의 흐름이 한 방향으로만 정렬돼 있어야 한다는 것. 공통 라이브러리가 저장소에 게시되면 서비스가 소비하고, 그 반대 방향은 일어나지 않습니다. 서비스끼리도 마찬가지라, 전체 의존이 순환 없는 하나의 DAG를 이룹니다. 이건 플러그인이 만들어 주는 성질이 아니라, 의존성을 설계할 때부터 지켜 온 제약입니다.
이게 왜 중요한지부터 짚겠습니다. "버전 전파를 자동화할 수 있는가"는 사실 "의존이 단방향인가"에 달려 있습니다. 흐름이 한 방향으로 정렬돼 있으면 배포 순서를 기계가 계산할 수 있고, 순환이 섞이면 — A를 올리려면 B가 먼저, B를 올리려면 A가 먼저인 교착이 생기면 — 그 계산 자체가 성립하지 않습니다. 자동화 도구가 얼마나 영리한지는 그다음 문제입니다.

그런데 이 예측 가능성에는 뒷면이 있습니다. 단방향성도, family 하나에 버전 하나라는 규칙도, 전부 "복잡도를 한 점에 응집시킨" 결과입니다. 각 레포가 자기 버전 규칙을 갖는 자유를 내려놓은 대가로, 조직 전체가 하나의 규칙 위에서 움직이게 됐습니다. 문제는 이 응집의 대가가 늦게 청구된다는 것입니다.
수백 개의 아티팩트가 이미 그 규칙 위에서 돌기 시작하면, 그 규칙은 함부로 바꿀 수 없는 것이 됩니다. group name policy의 층위를 다시 긋거나, family 경계를 재설계하거나, 게시 규칙을 손보는 일은 — 그 한 점을 건드리는 순간 그 위에 선 모든 레포가 동시에 영향받습니다. 한 점에 응집시켰다는 건, 그 한 점의 실수가 곧 전 조직의 동시 장애라는 뜻이기도 합니다. 예측 가능성과 비가역성은 같은 동전의 양면입니다. 이 문제는 도입할 때가 아니라 성공하고 한참 뒤에, 무언가를 바꿔야 할 때 비로소 모습을 드러냅니다.
§3. 세 번째 문제 — 성공하면 아무도 그걸 기억하지 않는다
가장 다루기 어려운 문제는 기술이 아니라 인식에 있습니다.
이 일이 잘 되면 무슨 일이 벌어질까요. "이 라이브러리 어느 버전 써요?"라는 질문이 사라집니다. 버전 조합이 안 맞아서 밤새 디버깅하던 일이 없어집니다. 슬랙에서 관리자를 붙잡고 "이거 지금 써도 돼요?"를 묻던 왕복이 사라집니다. 즉 성공의 증거가 "고통의 부재"라는 형태로만 남습니다. 그리고 없어진 고통은 아무도 기억하지 않습니다.
이건 이런 종류의 기반 작업이 조직에서 정당화되기 어려운 구조적 이유입니다. 새 기능은 데모할 수 있고, 성능 개선은 그래프로 보여줄 수 있습니다. 하지만 "원래 있었어야 할 마찰이 처음부터 없는 상태"는 보여줄 대상이 없습니다. 이 일에 시간을 쓰자고 설득하는 사람은, 아직 아무도 절실하게 느끼지 않는 미래의 고통을 근거로 현재의 투자를 요구해야 합니다. 그리고 일이 잘 끝나면, 그가 무엇을 막았는지조차 증명하기 어렵습니다.
저희가 이 문제를 완전히 풀었다고 말하진 않겠습니다. 다만 하나는 분명해졌습니다 — 이런 작업의 가치는 지표만으로는 온전히 보이지 않고, 그 부재를 알아보는 사람에게 먼저 보인다는 것. 지금 이 이야기를 읽으며 세 가지 문제에 고개를 끄덕이고 있다면, 아마 당신이 그런 사람일 겁니다.
그렇다면 조심스럽게 하나 여쭙고 싶습니다. 당신은 아마 이 문제를 이미 알고 있을 겁니다. "어느 버전 써요?"의 왕복도, 버전 조합이 어긋나 밤을 새운 일도, 슬랙에 흩어진 권위도 — 새삼 설명할 필요가 없을 만큼요. 그런데 그 문제는 지금도 당신의 조직에 그대로 남아 있을 가능성이 큽니다. 왜일까요. 몰라서가 아닙니다. 방금 이야기한 세 가지 문제 때문입니다. 닭과 달걀을 어디서 끊어야 할지 답이 안 서고(§1), 한번 세우면 되돌리기 어렵다는 걸 알기에 첫 삽을 못 뜨고(§2), 무엇보다 이걸 고쳐도 아무도 알아주지 않을 일이라는 걸 알기 때문입니다(§3). 그러니 미뤄둔 건 판단력이 부족해서가 아니라, 오히려 이 일의 난이도를 정확히 알기 때문에 내린 합리적인 유보였을 겁니다. 다만 — 그 유보의 비용은, 지금 이 순간에도 당신의 팀에 조용히 청구되고 있습니다.
§4. 그래서, 세 문제를 알고도 내린 선택
이 세 가지 문제를 알면, 앞서 한 문단으로 요약한 해법이 왜 그런 모양인지가 다르게 읽힙니다.
버전을 개별 아티팩트가 아니라 family 단위로 묶은 건, 관리 대상이 레포 수가 아니라 모듈 수만큼 불어나는 것을 막기 위해서입니다. 저희는 한 레포 안에 여러 모듈을 두고 각각을 별도 아티팩트로 배포합니다(헥사고날을 따르니 도메인 하나에서 배포 대상이 여럿 나옵니다). family가 없으면 같은 레포 안 모듈들끼리도 버전이 어긋나고, 소비자는 "이 모듈 3.1과 저 모듈 2.7이 같이 써도 되는 조합인가"를 매번 확인해야 합니다. family는 이 조합 폭발을 "하나의 family에 하나의 버전"으로 되돌립니다.

여기서 family가 필요해진 구조적 조건을 하나 더 짚고 싶습니다. 방금 "한 레포 안에 여러 모듈"이라고 했는데, 더 정확히는 한 레포 안에 여러 도메인이 들어갑니다. 한 백엔드 레포가 열 개가 넘는 독립 도메인을 담고, 각 도메인이 다시 자기 헥사고날 계층(모델·유스케이스·어댑터·저장소…)을 별도 모듈로 가집니다. 즉 레포 : 도메인 : 모듈이 1 : N : M입니다. 헥사고날을 패키지 네이밍이나 컨벤션으로 둘 수도 있지만, 저희는 그 경계를 각각 배포 가능한 Gradle 모듈로 쪼개 물리 경계로 강제했습니다. 의존 방향을 사람의 규율이 아니라 빌드가 막게 한 셈인데, 이 선택이 뒤에 올 이야기의 바탕이 됩니다.
이건 흔히 말하는 "폴리레포"가 아닙니다. 폴리레포라면 보통 레포 하나에 서비스 하나를 떠올리는데 우리 레포는 그보다 큰 그릇이고, 그렇다고 전 조직이 한 레포에 든 모노레포도 아닙니다 — 그런 레포가 수십 개니까요. 이름보다 중요한 건, 폴리냐 모노냐라 부르는 것보다 버전 관리의 단위를 물리적 레포 경계에서 떼어냈다는 점입니다.¹
version family가 family를 묶는 기준은 레포가 아니라 group name입니다. 그래서 한 레포 안에 서로 다른 family가 공존할 수도, 여러 레포에 걸친 아티팩트가 한 family로 묶일 수도 있습니다. "레포=버전 단위"라는 암묵적 등식을 끊어둔 덕분에, 레포가 여러 도메인을 담든 어떻게 쪼개지든 버전 체계가 흔들리지 않습니다. 폴리냐 모노냐의 논의가 레포 배치에서 멈추면, 정작 버전 단위가 무엇이어야 하는지는 설계 밖에 남기 쉽습니다.
버전 규칙을 group name policy로 레포 밖에 둔 건, §1의 순환을 감수하고 동형성에 베팅한 결과입니다. 규칙을 사람의 규율에 맡기면 사람이 50명일 때 예외도 50개 자라고, 문서가 낡는 속도보다 아티팩트 느는 속도가 빠릅니다. 규율은 조직이 작을 때는 충분했지만, 규모가 커지면 구조 없이는 버티기 어려웠습니다. 그래서 규율 대신 구조를 택했고, 그 구조의 비가역성(§2)은 대가로 받아들였습니다.
정리하면, 이 해법의 모든 선택은 "무엇이 편한가"가 아니라 "어느 문제를 감수할 것인가"의 결정이었습니다. 그리고 그 판단은 저희 조직의 규모와 제약 위에서만 정답입니다 — 보편적 처방이 아닙니다.
§5. 이 기반이 열어주는 것
세 가지 문제를 넘어선 건 끝이 아니라 시작입니다. 버전을 고를 필요 없이 늘 최신으로 수렴하고, 아티팩트가 한 방향으로만 흐르고, 배포 순서를 기계가 계산할 수 있는 상태 — 이건 그 자체가 목적이 아니라, 그 위에 다른 것을 마음 놓고 쌓을 수 있는 평평한 땅입니다. 땅이 평평하다는 걸 한번 믿을 수 있게 되면, 그 위에서 할 수 있는 일이 달라집니다.
버전 전파를 끝까지 자동화하는 파이프라인이 그 위에 섭니다(저희는 이걸 Evergreen이라 부릅니다). 라이브러리 하나를 고치면 그것을 쓰는 수십 개 레포에 변경이 순서대로, 검증을 거쳐 흘러갑니다. 그러면 무슨 일이 벌어질까요. 여기서 진짜 수혜자는 플랫폼을 만든 사람이 아니라, 그 위에서 제품을 만드는 동료들입니다.
공통 라이브러리를 고치면 수십 개 레포가 깨진다는 두려움 때문에, 나쁜 API도 그냥 두고, 개선을 미루고, 복사-붙여넣기로 우회하던 일들이 있습니다. 그 마찰이 사라지면 product engineer는 "이거 바꾸면 어디가 터지지?"를 걱정하는 대신 제품 자체에 집중할 수 있습니다. 좋은 추상을 만들어 공유하는 비용이 낮아지니, 바퀴를 다시 발명하지 않고 남이 만든 것 위에서 더 멀리 갑니다. 기반이 탄탄할수록, 그 위에 선 사람들은 더 가볍게 움직입니다.
그리고 지금은, 이 기반의 부재가 훨씬 더 빠르게 청구되는 시대입니다. AI가 코드를 대신 써 주기 시작하면서 "일단 복사해서 조금 고쳐 두자"는 그 우회가, 사람의 손이 아니라 생성 속도로 번집니다. 같은 로직의 조금씩 다른 사본이 레포마다 흩어지고, 원본이 무엇이었는지, 어느 것이 최신인지 아무도 답할 수 없게 되는 파편화가 실제 운영 비용이 됩니다. 코드를 빠르게 찍어낼수록, 그 빠름을 한 방향으로 수렴시켜 줄 땅이 없으면 파편화도 같은 속도로 커지니까요. 이 기반의 가치가 앞으로 더 커지는 이유가 여기 있습니다 — 빠름은 땅이 평평할 때만 축복이고, 그렇지 않으면 파편화를 가속합니다.

버전이라는 골치 아픈 문제를 구조로 지워낸 경험은, 그 자체가 목적이 아니라 그 위에 무엇을 세울지 고민하기 위한 출발선이었습니다. "이런 땅 위라면 어떤 플랫폼을 더 세울 수 있을까, 그러면 그 위에서 제품을 만드는 동료들이 지금보다 얼마나 더 가볍게 움직일 수 있을까." 저희가 흥미를 느끼는 질문은 늘 그 다음 층에 있습니다.
(본문의 레포명·group name·버전 문자열은 설명을 위한 예시이며 실제 식별자가 아닙니다. 정량적 서술은 저자 팀의 자체 운영 경험에 근거합니다.)
¹ 용어에 대하여
이 중간 지대를 부르는 표준 용어는 아직 없습니다. Nx는 단일 모노레포와 완전 폴리레포 사이를 multiple monorepos라 부르고, Microsoft는 TypeScript와 Azure에서 하이브리드 저장소 구성을 사용합니다. Google은 전 조직을 하나의 모노레포로 운영하며, 그 일관성을 기반으로 대규모 코드 변경을 원자적으로 적용합니다(Rachel Potvin & Josh Levenberg, Why Google Stores Billions of Lines of Code in a Single Repository, CACM, 2016). 저희는 단일 모노레포 대신 빌드 컨벤션과 group name policy를 통해 같은 수준의 일관성을 확보했습니다.
🚀플렉스팀 채용페이지 바로가기
☕flex Private Talk 신청하기