파이썬 패키징 #7 팀 규약과 도구 선택: uv, poetry, pip
혼자 쓸 때의 패키징은 취향 문제지만, 팀에서는 규약 문제가 됩니다. 시리즈 마지막 편은 그 규약을 세우는 데 필요한 두 가지, 도구를 고르는 기준과 팀·CI·도커에서의 표준 패턴을 정리하고 전체를 회고합니다.
도구 선택: 세 후보를 한 표에 #
2026년 현재 실질 후보는 셋입니다.
| 구분 | uv | poetry | pip + venv |
|---|---|---|---|
| 잠금 | uv.lock (기본) | poetry.lock (기본) | 별도 도구 필요 (pip-tools) |
| 파이썬 버전 관리 | 내장 (uv python) | 없음 (별도 도구) | 없음 (별도 도구) |
| 속도 | 기준점 (가장 빠름) | 수 배 느림 | lock 컴파일까지 하면 크게 느림 |
| 표준 준수 | pyproject.toml + PEP 735/751 | 2.x부터 표준 수렴 | 표준의 기준점 |
| 강점 | 통합 워크플로, 속도 | 성숙한 생태계, 오랜 검증 | 어디에나 있음, 의존성 0 |
판단은 상황별로 갈립니다.
- 신규 프로젝트는 uv입니다. #4에서 본 대로 환경·잠금·파이썬 버전까지 한 도구로 끝나고, 생태계의 무게 중심도 이미 이쪽입니다.
- poetry로 잘 굴러가는 팀은 서두를 이유가 없습니다. poetry는 2.x에서 표준 형식에 수렴했고 활발히 유지되고 있습니다. 마이그레이션은 도구 교체 자체보다 팀원 습관과 CI 스크립트 전환이 비용이므로, “다음 신규 프로젝트부터 uv"가 현실적인 전환 경로입니다.
- pip + venv가 정답인 자리도 남습니다. 외부 도구를 설치할 수 없는 제약 환경, 표준 라이브러리만으로 굴러가야 하는 최소 구성이 그렇습니다. 이 경우 잠금은 pip-tools나 #5에서 본 표준 pylock.toml로 보완합니다.
피해야 할 것은 도구 자체가 아니라 혼용입니다. 한 저장소에서 누구는 pip install, 누구는 uv add를 쓰면 선언과 lock이 어긋나기 시작합니다. 저장소마다 도구 하나를 정하고 README 첫 줄에 못 박는 것이 규약의 시작입니다.
CI: 캐싱과 frozen 검증 #
테스트 #7의 CI에 패키징 규약을 얹으면 이런 모양이 됩니다.
# .github/workflows/ci.yml (핵심부)
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true # uv 캐시를 러너 간에 재사용
- run: uv sync --frozen # lock 그대로 재현, 어긋나면 실패
- run: uv run pytest포인트는 둘입니다. 캐시는 uv의 전역 캐시를 CI 러너 간에 재사용해 설치 시간을 초 단위로 줄입니다. --frozen은 #5에서 세운 원칙의 집행 장치로, lock 갱신을 잊은 PR을 CI가 잡아냅니다. 여기에 월 단위 의존성 업그레이드 PR(Renovate·Dependabot)과 pip-audit 스캔까지 걸면 #5의 운영 루틴이 전부 자동화됩니다.
도커: 이미지에서도 같은 규약으로 #
프로덕션 이미지에서도 원칙은 같습니다. lock 그대로, 개발 의존성 없이, 캐시를 살려서입니다.
FROM python:3.13-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
# 의존성 레이어: lock이 안 바뀌면 캐시 히트
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project
# 앱 레이어: 코드만 바뀌면 여기부터 재빌드
COPY . .
RUN uv sync --frozen --no-dev
CMD ["uv", "run", "python", "main.py"]의존성 설치와 코드 복사를 레이어로 분리하는 것이 핵심입니다. 코드만 고친 빌드에서는 의존성 레이어가 캐시로 통과해 빌드 시간이 크게 줄어듭니다. --no-dev는 pytest, ruff 같은 dev 그룹을 프로덕션 이미지에서 빼 줍니다. #3에서 extras와 dependency-groups를 갈라 둔 것이 여기서 값을 합니다.
팀 규약 체크리스트 #
새 팀원이 합류해도 무너지지 않는 상태를 기준으로 정리하면 이렇습니다.
- 도구는 저장소당 하나: README에 명시하고, 다른 도구의 명령을 문서에서 지웁니다.
- 커밋 대상: pyproject.toml과 uv.lock은 커밋,
.venv는.gitignore. lock 없는 PR은 CI가 거부합니다. - 선언은 범위, 잠금은 정확히: 직접 의존성만 pyproject.toml에, 전이 의존성은 lock에 맡깁니다.
- 파이썬 버전은
.python-version으로 고정: “저는 3.12인데요"가 나오지 않게 합니다. - 업그레이드는 독립 PR + 정기 루틴: 기능 PR에 lock 변경이 섞여 있으면 리뷰에서 분리를 요구합니다.
- CI는 frozen, 프로덕션은 no-dev: 재현은 기계적으로, 개발 도구는 이미지 밖으로.
시리즈를 마치며 #
일곱 편의 여정을 되짚으면 이렇습니다.
- #1 가상 환경: 환경이 꼬이는 구조적 원인과 venv, activate의 정체를 봤습니다.
- #2 pip와 requirements.txt: 전통 워크플로를 익히고, 의도와 스냅샷이 한 파일에 섞이는 한계를 확인했습니다.
- #3 pyproject.toml: 의도가 사는 표준 거처와 extras·dependency-groups의 구분을 익혔습니다.
- #4 uv: 환경·설치·잠금·파이썬 버전이 한 도구로 접히는 현대 워크플로를 봤습니다.
- #5 의존성 잠금: uv.lock을 직접 읽고 frozen 원칙과 업그레이드 루틴, 표준 pylock.toml을 다뤘습니다.
- #6 배포: 받는 쪽에서 만드는 쪽으로 서서 wheel 빌드와 PyPI, Trusted Publishing을 익혔습니다.
- #7 팀 규약: 그 전부를 도구 선택 기준과 CI·도커·팀 체크리스트로 묶었습니다.
패키징은 화려한 주제가 아닙니다. 하지만 “클론하고 sync 하면 누구의 머신에서든 같은 환경이 선다"는 상태는, 그 위에 올라가는 모든 코드·테스트·배포의 바닥입니다. 이 시리즈가 그 바닥을 단단하게 만드는 데 쓰이기를 바랍니다. 이 위에 무엇을 지을지는 이미 준비되어 있습니다. 테스트로 코드를 지키고, 자동화로 반복을 지우고, 데이터 분석으로 질문에 답하는 시리즈들이 전부 이 바닥 위에서 동작합니다.