Python&FastAPI 위주로 MSA 를 구성하면서 codebase 는 monorepo 형태를 차용했습니다. 이 글에서는 Python으로 MSA를 만들 때 uv를 통해 어떻게 monorepo 형태로 구성할 수 있는지 경험을 공유하고자 합니다.
monorepo 란?
monorepo 는 polyrepos(혹은 multi-project)에 대응되는 개념으로, 하나의 Git 저장소에서 여러 모듈(서비스‧라이브러리)을 함께 관리하는 전략을 말합니다. 동일한 논리적 변경을 하나의 Commit 또는 PR(Review)로 묶을 수 있고, 저장소별 권한·CI/CD 파이프라인을 중복 정의할 필요가 없다는 점이 가장 큰 장점입니다.
반면 “거대한 모놀리식 시스템(모놀리식 코드베이스)과 같다”는 오해도 있지만, 코드 관리 방식과 런타임 아키텍처는 분리해서 봐야 합니다. 모놀리스는 배포 단위가 하나지만, monorepo는 여러 독립 실행 단위(컨테이너·람다·패키지)를 한 저장소에 둘 뿐입니다.

CircleCI의 글에서는 monorepo 채택에 따른 이점을 다음과 같이 제시합니다.
- 원자적 변경(Atomic change): API 스펙 변경과 각 서비스 클라이언트 수정이 한 커밋에 들어가므로 깨지는 중간 단계가 사라짐
- 일관된 개발 경험: 공통 린터, 테스트, 릴리스 스크립트를 한곳에 유지
- 쉬운 코드 리팩터링: 대규모 네임스페이스 변경·공통 모듈 추출을 한 PR에서 끝낼 수 있음
실제 우리 팀은 monorepo 를 도입했을때 위의 이유로 개발 편의성이 향상되고 이로인해 human error 발생 빈도를 줄일 수 있었습니다.
monorepo 와 multi-module
Gradle 기반 SpringBoot를 쓰던 저에게는 multi-module이 더 익숙한 개념이었고 monorepo 라는 개념을 받아들일때 도대체 multi-module과 다를게 뭔가 궁금했습니다. 결국 저는 “단일 코드베이스에 여러 모듈”이라는 점은 동일하지만, • multi-module은 빌드 시스템(Gradle) 관점, • monorepo는 저장소‧협업 관점의 용어라는 차이가 있을 뿐이라고 결론을 내렸습니다. 이 글에서는 통일하여 monorepo라 부르겠습니다.
패키지, 프로젝트 관리의 필요성
monorepo 구조까지는 좋았습니다. 하지만 우리 팀은 { polyrepo, Monolithic, Java&SpringBoot }에서 { monorepo, MSA, Python&FastAPI }로 전환을 하였고 대대적인 패러다임·언어·아키텍처 변화를 겪다보니 제대로된 “패키지 관리와 프로젝트 관리”를 하지 못하고 있었습니다.
처음에는 pip-tools 를 이용해 개별 서비스마다 별도의 requirements.in 과 requirement.txt 파일을 두고 local 개발환경에서 개별 서비스마다 가상환경(venv)을 별도로 지정하게 되었습니다. 처음 두세번의 배포까지는 아무런 문제가 없었습니다. 그런데 점점 서비스간 패키지 버전이 불일치하는 일이 발생했습니다. 서비스간 패키지 버전이 불일치한다는 것은 A 서비스에서는 제공되는 기능이 B 서비스에서는 제대로 동작하지 않는문제가 발생할 수 있다는 것입니다.
개별 서비스마다 별도의 가상환경을 두는 선택이 문제를 더욱 심각하게 만들었습니다. local 개발환경에서 A 서비스의 가상환경을 환경을 활성화해놓고 B 서비스의 코드를 수정, 테스트하면 분명 문제없이 동작하던 기능이 (A 가상환경이었기 때문에) 컨테이너화하여 Pod 에 올라가면 K8s의 Liveness probe 에서 막히는 일이 발생했으며, 더 심각하게는 전혀 예상치 못한 RuntimeException 이 발생하게 되었습니다.
물론 컨테이너화해서 테스트하는 절차를 철저하게 지킨다면 위의 문제가 발생하기 전에 알아차릴 수 있었겠지만 Test Coverage 를 적정 수준으로 유지해야하므로 현실적으로 이런 상황에서 모든 RuntimeException을 막기는 불가능합니다.
따라서 패키지간 버전 불일치 문제를 해결하고 local 환경에서 하나의 가상환경에서 여러 서비스를 개발할 수 있는 패키지, 프로젝트관리 체계가 필요해졌습니다.
uv 로 monorepo 만들기
왜 uv인가?
결과적으로 우리 팀은 uv 와 workspace, build-system을 활용하여 이러한 문제를 해결했습니다. 그런데 Python 생태계에는 많은 패키지 관리 툴이 존재합니다. 그 중에서 왜 uv 를 선택했을까요? 저희는 제대로된 패키지 관리 체계를 도입하기 위해 아래의 요구사항을 세웠습니다.
우리가 세운 최소 요구사항
요구사항을 만들다보니 "공통 모듈" 이라는 개념이 추가 되었는데 이것 역시 MSA를 운용하다보면 필요성이 절실해지는 부분입니다.
- 아키텍처(arm64/amd64)·OS·Python 버전별 재현 가능한(Immutable) Lockfile
- 모든 마이크로서비스가 단일 Lockfile을 공유해 버전 불일치를 막을 것
- 공통 모듈을
shared/하위 패키지로 두고 필요한 서비스가 “로컬(Editable) 의존성”으로 끌어다 쓸 것 - 의존성 해석과 설치 속도가 빠를 것(로컬 캐시·병렬 빌드 지원)
- Docker나 CI 환경에서 pip만큼 가볍게 쓸 수 있을 것(
curl /uv한 줄 설치)
패키지 관리툴 간단 비교
이러한 기준을 바탕으로 알려져있는 몇몇 패키지 관리툴을 비교하였습니다.
| 툴 | Lockfile | Workspace/Monorepo | 패키지 설치속도 |
| pipenv | pipfile.lock | 미지원 | 느림 |
| Poetry | poetry.lock | Partial(패키지 경로) | 보통 |
| PDM | pdm.lock | PEP 582 기반 (글로벌 Cache) | 빠름 |
| Hatch | hatch.lock | 플로그인으로 지원 | 보통 |
| uv | uv.lock | Cargo Workspace 개념 지원 | 매우 빠름 |
uv는 Rust로 작성되어 pip 대비 8~10배, Poetry 대비 4~5배 빠른 의존성 해석·설치 속도를 보여줍니다. 또한 Cargo와 같은 workspace 기능을 제공해, 여러 패키지가 하나의 uv.lock을 공유하도록 강제합니다.
Reddit 등 커뮤니티 피드백에서도 “Poetry와 기능 동등성(parity)을 달성했고, monorepo 이동 시 버전 지옥을 해소했다”는 후기가 많았으며, 한국의 Python 사용자들도 uv 를 적극권장한다는 이야기를 전했습니다.
우리가 원하는 요구사항을 모두 충족하면서, 속도도 빠르고, 국내외 사용자들의 후기도 좋았기에 uv를 제외한 다른 툴을 선택할 이유가 없었습니다.
uv 의 주요 개념
1. pyproject.toml, uv.lock 파일
사실 pyproejct.toml 파일과 lock 파일은 uv 만의 개념은 아닙니다. 하지만 Python 패키지 관리 툴을 처음 접하는 사람에게는 둘다 생소한 개념이므로 이 글에서 설명합니다.
pyproject.toml 파일은 개념적으로 이해하자면 개발자가 원하는 명세를 작성하는 파일입니다. 이 프로젝트의 이름과 설명, 상황별로 원하는 라이브러리와 그 버전을 명시합니다. 필요하다면 Lint 설정도 가능합니다. 그리고 이러한 개발자가 작성한 명세에 따라 실제 원하는 것들을 모아서 그 상태를 저장한 파일이 uv.lock 파일입니다.
실제 Python 어플리케이션이 구동하기 위한 가상환경의 구성은 1차적으로 uv.lock 파일에 의존합니다. 만약 uv.lock 파일이 없다면 pyproject.toml 파일을 통해 바로 uv.lock 파일을 생성할 수 있습니다.
이 글에서는 개념정도만 잡고 두 파일에 대한 구체적인 설명은 pyproject.toml 공식문서, uv.lock 공식문서를 참조하시는걸 추천드립니다.
2. workspace
uv에는 Rust Cargo Workspace 개념이 적용되었습니다. Rust의 패키지관리 툴인 Cargo에서 Workspace란 크기가 커진 Rust 라이브러리를 잘게 쪼개서 관리할 수 있도록, 즉 크기가 작아진 개별 라이브러리간 상호 참조를 손쉽게 할 수 있도록 도와주는 장치입니다.
uv에서 동일한 workspace에 '공통 모듈'과 '마이크로서비스'들이 등록된다면 동일한 uv.lock 파일을 공유하게되고 마이크로서비스들이 공통 모듈을 손쉽게 참조할 수 있는 관계를 만들 수 있게 됩니다. 이러한 참조 관계는 path 참조를 통해서도 이뤄질 수 있으며 혹은 가상환경안에 해당 공통모듈을 설치하여 참조할 수도 있습니다.
3. build-backend
build-backend 역시 대부분의 Python 패키지 관리 툴이 공유하는 개념으로 어떤 방식으로 해당 패키지를 빌드해서 가상환경에 추가할지를 지정하는 것입니다. 그런데 설명이 좀 이상합니다. Python은 인터프리터 언어이므로 빌드라는 과정이 없는데 도대체 뭘 빌드한다는 걸까요?
더 엄밀하게 설명하자면 build-backend 들은 Python 코드를 가진 소스 디렉터리를 다른 환경에서 설치가 가능한 아카이브(.whl, .tar.gz)로 변환하는 과정을 빌드라고 말합니다. 이렇게 아카이브로 변환하는 과정을 거쳐야 패키지 설치 속도와 운영체제 호환성이 확보됩니다.
uv 를 이용한 MSA monorepo 예시
예시 코드는 깃헙에서 확인하실 수 있습니다.
핵심은 공통모듈은 모두 build-backend 를 활용하여 아카이브 파일 형태로 가상환경에 포함된다는 것입니다. 이러한 조치를 통해 각 서비스가 배포되는 환경이 변화되어도 안정한 동작을 보장할 수 있고, 무엇보다 IDE가 해당 패키지의 존재 여부를 명확하게 파악할 수 있습니다.
필요하다면 마이크로서비스로 아카이브파일로 바꿔서 사용하는 방법을 구현할 수도 있겠으나, 현재는 Dockerfile 에서 해당 파일을 직접 호출하는 형태이므로 불필요하다고 판단했습니다.
참고
https://blog.nrwl.io/misconceptions-about-monorepos-monorepo-monolith-df1250d4b03c
https://engineering.linecorp.com/ko/blog/mono-repo-multi-project-gradle-plugin
https://techblog.lycorp.co.jp/ko/python-multi-project-application-with-poetry
'탐구 생활 > Python Monorepo' 카테고리의 다른 글
| Python Monorepo - uv Docker 다이어트하기 (0) | 2025.06.29 |
|---|---|
| Python Monorepo - namespace 활용하기 (0) | 2025.06.14 |