나무모에 미러 (일반/밝은 화면)
최근 수정 시각 : 2025-09-06 14:52:53

모노레포


1. 개요2. 원리3. 장점4. 한계 및 비판5. 오해6. 관련 소프트웨어7. 관련 문서

1. 개요

mono-repository, monorepo

상호 종속성(dependency)을 가지는 다수의 논리적 패키지를 하나의 저장소(repository)에서 단일화하여 관리한다는 소스 코드 구성 방법론(methodology) 및 기술적 신조어의 일종. 유일함을 뜻하는 mono-[1]버전 관리 시스템의 소스 코드 저장소(repository[2])를 합성한 단어이다.

반대되는 개념은 polyrepo[3]로, 각각의 논리적 패키지가 단 하나의 버전 관리 체계 및 저장소와 1:1 관계로 대응되는 '전통적인' 구성 형태를 의미한다.

2. 원리

모노레포 방법론에서는, 단 하나의 버전 관리 시스템 안에 각자의 버전을 관리하는 다수의 패키지가 있다고 가정하는 것부터 시작한다.

이 패키지는 재사용 가능한 코드를 의미하며, 보통 언어에 따라 모듈이나 라이브러리 등의 이름을 사용하지만, 빌드 시스템이나 번들러가 지원하는 한 설정 파일이나 스키마 DSL, 이미지 등 에셋이 되는 것도 가능하다. 구체적으로는 UI 쪽의 경우 컴포넌트나 컴포넌트 라이브러리(또는 디자인 시스템), API 클라이언트, 공통 스키마 및 validation 코드, 재사용 미들웨어, 파서 등이 있다. 또한 언어에 따라 '실행 가능한 모듈'도 패키지로 인식되기도 하는데, 구체적으로는 백엔드, 프런트엔드, 인증 서버, 각 microservice, CLI 프로그램 등 종속성 형태로 재사용되지 않고 온전히 쓰이는 end application을 의미한다.

일반적으로, 각각의 패키지는 다음 파이프라인을 거쳐 빌드가 수행된다. 편의상 실패는 가정하지 않는다. 라이브러리 패키지의 경우 publish를 하고, 실행 가능한 패키지의 경우 deploy를 진행한다고 가정한다.
종속성 확보 -> 빌드 -> 배포(publish/deploy)

모노레포를 사용할 경우, 필요한 모든 종속성마다 위 파이프라인을 전부 거칠 필요 없이, 배포를 생략하고 빌드까지만 진행한 상태로 넘어갈 수 있다. 필요한 패키지가 이미 빌드가 끝난 경우, 캐시에서 아티팩트만 가져온다.

이때 공통된 패키지 하나를 수정한다고 생각해 보자. 가령 프런트 패키지(F)와 백(B)에서 모두 참조하는 스키마 패키지 S를 수정해 새 버전인 S'을 만들고, 별도의 배포 과정 없이 기존 S에 의존하던 F와 B에서 새 스키마를 반영해 각각 F'과 B'이라는 새 버전을 만들 수 있다. 캐싱이 올바르게 수행된다고 가정할 때, 로컬에서 새로 바뀐 코드베이스를 구동하기 위해선 S'과 각 애플리케이션인 F', B'만 빌드하면 된다.

이제 해당 변경사항을 버전 관리 시스템에 기록하고 캐시를 사용하는 CI로 올렸을 때, 모노레포 레이어에서는 코드베이스의 수많은 패키지 중 새로 변경된 S', F', B'만 구분해 내어 이들을 빌드하고, 배포 환경에는 기존 돌아가던 이메일 서버(E), 인증 서버(A), 로그 싱크(L) 등등은 놔두고 F, B만 찾아내어 중지시킨 다음, F', B'을 새로 배포하는 방식이다.

3. 장점

4. 한계 및 비판

결국은 canonical한 빌드 구성에 또다른 추상화 레이어가 추가되는 것이니 이에 따른 비용을 감수해야 한다. 대표적으로 개별 패키지의 독자적인 버전 관리, 로컬 reference와 레지스트리에 올라가는 패키지 두 사용 방식의 동시 관리 비용 등의 문제가 있다. 이러한 것을 암만 자동화하는 이상적인 구조와 CI/CD 시스템을 구축한다 해도 이 또한 추가적인 레이어이며, 이러한 레이어를 구축하고 유지하는 과정에 다양한 비용이 발생하는 것을 막기 힘들다.

모노레포의 대표적인 장점이 내부 패키지를 shallow하게 참조하면서 모든 패키지 그래프를 동일한 버전 관리 저장소의 동일한 스냅샷에 담아 동기화에 대한 문제를 해결한다는 것인데, 당연하게도 이는 양날의 검이 된다. 예를 들어 패키지 A와 B가 공통 패키지 C를 참조할 때, 이 C의 버전은 주어진 커밋에 대해 유일하게(uniquely) 하나여야 하며, A가 참조하는 C와 B가 참조하는 C의 버전이 다를 수 없다.# 다시 말하면 single-version policy를 internal package에도 강제한다는 것인데, 이상적인 프로젝트 관리 방법이긴 하지만 현실적으로 적용이 어려운 경우가 많다.# 가령 공통 패키지 C가 breaking change를 가질 때, 나머지 패키지는 전부 C의 새 버전으로 업데이트 했지만 A의 경우 리팩토링에 시간이 오래 걸려 당장은 새 버전으로 업데이트 할 수 없는 경우, A는 C의 구 버전을 참조하는 동시에 다른 패키지는 C의 최신 버전을 참조해야 하나 모든 패키지가 동일한 버전 관리 히스토리에 묶이게 되는 일반적인 모노레포 환경에서는 이러한 구성이 불가능하다. 외부 레지스트리를 사용해 해결할 수 있으나 그러면 해당 저장소는 단지 code colocation일 뿐 사실상 polyrepo랑 근본적으로 다를 게 없다.

모든 프로젝트, 프레임워크 유형이 모노레포 방법론에 맞는 것은 아니다. 어떤 언어는 배포나 빌드가 너무 간단해서 개선이 필요 없고, 어떤 언어는 종속성의 버전 관리 체계가 모노레포와 어울리지 않기도 한다. 현실적으로 대부분의 monorepo 구현체는 사실상 거의 대부분이 JavaScript에 특화되어 있는데, 이는 태생적으로 모듈 시스템의 표준화가 너무 느렸던 JavaScript만의 현실을 과대 해석했을 뿐이라는 비판도 있다. 근래 등장한 모던한 언어의 경우 이미 내장된 빌드 시스템이 모노레포의 사상 일부를 이미 내재하고 있기 때문.

위의 단점에 이어, 패키지간의 사용 언어 자체가 호환이 되지 않는다면 모노레포 시스템은 일반적인 구축이 불가능하는 한계가 있다. 가령 Flutter로 클라이언트를 짜고 Go로 서버를 짜는 경우 등. 이 경우 서로간의 통신 규약 빼고는 대부분의 비즈니스 로직 관련 코드를 공유할 수 없을 텐데, 통신 규약 레이어도 일반적으로 스키마 정도만 공유할 수 있지 런타임 라이브러리를 공유하는 수준은 불가능하다. 사실 위와 같은 시나리오의 경우 대부분 monorepo 도입 논의 자체가 무의미하며,# 필요하다면 서버와 클라이언트 각각의 도메인에서 모노레포를 구축할 수는 있으나, 전체 프로덕트 하나를 아우르는 모노레포는 성립이 불가능하다. 폴리글랏한 내용으로 구성된 모노레포는 대부분 e2e test 등 수준보다 깊은 종속성을 제공하지는 않으며, 이런 정도의 CI는 사실 레포가 하나가 아니어도 구축이 가능하다. JavaScript 특성상 다양한 환경에서 사용할 수 있기에 흔히 발생하는 오인 중 하나.

사실 이같은 우회적인 레이어가 필요해지는 근본적인 원인이 JavaScript 생태계 자체의 패키징 시스템이 과도하게 레지스트리 종속적이고, direct Git deps 지원이 워낙 부실하기 때문이기도 하다.#2974 Git을 패키지 배포 및 resolution 과정에 적극적으로 활용하는 Go 모듈 시스템 등의 경우는 이러한 문제가 발생하지 않는다. 반면에 이미 그 자체로도 TypeScript, 번들러, 트랜스파일러 등 상당한 빌드 레이어가 요구되는 JavaScript 생태계에서는 Git deps라는 개념 자체가 유명무실한 것도 사실이다. 때문에 소스를 반드시 '어떤 예측 불가능한 파이프라인'에 따라 빌드 및 publish하는 과정이 반드시 요구되게 되고, 이러한 상황 속에서 monorepo가 적절한 해결책으로 등장하게 되었다는 것.

저장소라는 순수하게 논리적인 개념이 monorepo에서는 정작 GitHub 등 Git 호스팅 서비스에서 제공하는 관습적 인터페이스에 영향받았다는 비판 또한 존재한다. 가령 한 org 내에서의 flat한 구조라던가, 보통 레포 하나당 별도의 이슈 트래커나 CI가 하나씩만 주어진다던가 하는 비필수적 제약사항들이 은근히 암시적으로 반영되어 있다는 것. 사실 서브모듈을 이용한다던가 하면 모노레포 없이도 거의 똑같은 매커니즘을 구축할 수 있다.

사소한 한계이기는 하나 결국은 하나의 저장소라는 단위에 모든 코드가 묶이기 때문에 플랫폼에서 제공하는 저장소 단위 기능에 제약을 받을 수도 있다. 가령 polyrepo 상황에서는 특정 코드는 private, 특정 코드는 public으로 관리할 수 있지만 당연히 모노레포에서 이런 건 힘들다.

5. 오해

MSA 쪽 개념이랑 혼동해서 monorepo == monolithic architecture라는 오해를 사기도 하는데, 엄밀히 따지면 둘은 전혀 다른 레이어로, 서로 어떠한 연관도 없다. MSA는 소프트웨어 아키텍처링 방법론의 일종이고 반대로 monorepo는 소스 코드 구성 방법론의 일종으로, 소스 코드를 어떤 방식으로 관리하든 결과로 도출되는 아키텍처는 변함이 없어야 한다. 대신 현실적으로 MSA를 쓰면 monorepo 도입도 고민하게 될 가능성이 높아지는 것은 어느 정도 사실으로, 각 microservice별로 공통된 코드, 가령 공유 비즈니스 로직이나 gRPC schema 등 공유하는 코드 덩어리가 있다면 이들과 각 service들을 하나씩 별개의 레포로 관리하느니 하나의 프로덕트 monorepo를 만들고 그 아래에 전부 몰아담는 게 직관적이긴 하다.

monorepo를 사용하면 저장소가 너무 무거워진다거나, 받아야 할 커밋이 너무 늘어난다는 등의 비판이 따르기도 하는데, 사실 크게 문제가 되지 않는다. 무거운 파일이 있으면 필히 LFS를 쓰면 되고, 그래도 저장소가 너무 무겁다면 폴더 일부만 sparse checkout을 해서 작업하면 된다.#

6. 관련 소프트웨어

7. 관련 문서


[1] 어원은 고전 그리스어 μόνος[2] 어원은 보관함을 뜻하는 라틴어 repositōrium[3] 이쪽은 πολύς(많은)+repositōrium(보관함)