uv Workspaces와 AWS Lambda를 활용한 Python Monorepo 관리

발행: (2026년 1월 17일 오전 02:31 GMT+9)
8 min read
원문: Dev.to

I’m happy to translate the article for you, but I need the text you’d like translated. Could you please paste the content of the article (or the specific sections you want translated) here? Once I have the text, I’ll provide a Korean translation while preserving the original formatting, markdown, and code blocks.

Source:

UV Workspaces – A Quick Overview

UV workspaces는 특히 모노레포 환경에서 상호 연결된 Python 패키지를 개발할 때 슈퍼‑툴입니다.
레포 루트에 pyproject.toml이 있고 서브 폴더 안에서 uv init을 실행하면 UV는 다음을 수행합니다:

  1. 모든 프로젝트의 종속성을 포함하는 단일 가상 환경을 생성합니다.
    IDE에 친화적 – 가상 환경을 계속 전환할 필요가 없습니다.
    ⚠️ 중요한 주의사항: 아래를 참고하세요.

  2. [tool.uv.sources]에 로컬 프로젝트를 추가하여 편집 가능한 패키지로 설치합니다. 따라서 변경 후마다 재빌드/재설치할 필요가 없습니다.

[tool.uv.sources]
common_logging = { workspace = true }

Caveats

CaveatDescription
Conflicting dependencies워크스페이스 구성원 중 하나라도 호환되지 않는 버전을 요구하면 uv sync가 실패합니다. 마이크로서비스 환경에서는 일반적으로 호환 가능한 버전으로 종속성을 맞추는 것이 좋습니다.
IDE false‑positives가상 환경이 하나뿐이기 때문에 IDE가 실제로 해당 서비스의 종속성에 선언되지 않은 import를 제안할 수 있습니다. 이는 로컬 개발 중에 눈에 띄지 않을 수 있습니다. 이런 경우 워크스페이스 항목 대신 경로 종속성을 사용하는 것이 좋습니다:
[tool.uv.sources]
common_logging = { path = "../common/logging", editable = true }

CI / 프로덕션에서 UV 워크스페이스 사용

저는 계속 UV 워크스페이스를 사용하고 있지만, 코드가 프로덕션에 도달하기 전에 “conflict” 문제를 잡아내는 CI 검사가 있습니다.

아래 예시는 AWS Lambda 컨테이너 이미지를 전제로 하며, /var/task/var/lang/lib와 같은 경로를 설명합니다. 동일한 접근 방식은 Lambda가 아닌 컨테이너에서도 작동합니다; 단지 베이스 이미지와 경로만 조정하면 됩니다.

두 가지 핵심 요구사항

  1. 선택적 설치 – 모든 워크스페이스 의존성을 모든 마이크로서비스에 설치할 필요는 없습니다.
  2. 레이어 캐싱 – 핵심(공유) 의존성은 서비스‑특정(로컬) 의존성과 별도의 Docker 레이어에 존재해야 합니다. 이는 이미지 크기와 레이어 캐싱이 콜드‑스타트 성능에 직접 영향을 주기 때문에 Lambda 이미지에서 중요합니다.

Docker Build – 단계별

아래는 정리된 재현 가능한 Dockerfile입니다:

  • 필요한 pyproject.tomluv.lock 파일만 복사합니다.
  • --package--no‑install‑local 옵션을 사용해 핵심 의존성(워크스페이스 전체)을 설치합니다.
  • 워크스페이스 그래프를 기반으로 로컬 의존성을 설치합니다.
  • 최종 서비스 소스 코드를 복사합니다.
# -------------------------------------------------
# Build Stage – install dependencies with UV
# -------------------------------------------------
FROM public.ecr.aws/lambda/python:3.14-arm64 AS builder

# Install UV (binary from the official image)
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

ARG SERVICE_NAME

# Working directory for the build
WORKDIR /build

# -------------------------------------------------
# 1️⃣ Copy dependency definition files
# -------------------------------------------------
COPY pyproject.toml uv.lock /build/
COPY services/${SERVICE_NAME}/pyproject.toml /build/services/${SERVICE_NAME}/pyproject.toml

# -------------------------------------------------
# 2️⃣ Install **core** (shared) dependencies
# -------------------------------------------------
# --no-install-local  → ignore other workspace members
# --no-dev            → skip dev dependencies
# --package ${SERVICE_NAME} → install only the package we care about
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --no-install-local --no-dev --package ${SERVICE_NAME}

# -------------------------------------------------
# 3️⃣ Install **common** (local) dependencies
# -------------------------------------------------
# Export the exact requirements for the service, filter only the
# `services/common` entries, then install them with pip into a clean target.
RUN uv export --package ${SERVICE_NAME} \
    --no-editable --no-dev --frozen --format requirements.txt \
    | grep '^./services/common' > common.txt

RUN --mount=type=bind,source=services/common,target=/build/services/common \
    uv pip install --no-deps -r common.txt --target /build/common

# -------------------------------------------------
# 4️⃣ Copy the application code (service source)
# -------------------------------------------------
COPY services/${SERVICE_NAME}/src /build/app

최종 런타임 이미지

런타임 이미지는 서비스에 필요한 오직 핵심 의존성, 공통(로컬) 의존성, 그리고 서비스 코드를 포함합니다. UV 바이너리, 빌드 시 캐시, 중복된 소스 복사본은 포함되지 않습니다.

# -------------------------------------------------
# Runtime Stage – minimal image
# -------------------------------------------------
FROM public.ecr.aws/lambda/python:3.14-arm64 AS runtime

ARG SERVICE_NAME

# Copy the prepared environment from the builder
COPY --from=builder /build/common   /var/task/
COPY --from=builder /build/app      /var/task/

# (Optional) Set PYTHONPATH if you need to point to the venv location
# ENV PYTHONPATH=/var/task/.venv/lib/python3.14/site-packages

# The Lambda base image automatically looks for a handler in /var/task
# No further commands are required.

왜 이 2단계 접근법인가?

  • 깨끗한 최종 이미지 – 런타임 의존성만 남고, 빌드 시 생성된 아티팩트(UV, 캐시, 중복 소스 트리)는 남지 않습니다.
  • 캐시 친화적인 레이어 – 핵심 의존성은 거의 변경되지 않으므로 Docker가 해당 레이어를 재사용할 수 있습니다. 로컬/공통 의존성은 별도 레이어에 설치하고, 서비스 코드는 마지막 레이어에 두어 코드만 변경될 때 빠르게 재빌드됩니다.
  • 해결은 UV, 설치는 pip – UV가 의존성 해결의 무거운 작업을 수행하고, pip install --target …가 Lambda 런타임이 직접 사용할 수 있는 단순하고 독립적인 site‑packages 디렉터리를 생성합니다.

TL;DR

  • UV workspaces는 전체 레포지토리에서 단일 venv를 제공하므로 IDE 사용성이 뛰어납니다.
  • 충돌하는 의존성IDE 오탐에 유의하세요; 필요하면 경로 의존성을 사용합니다.
  • Docker/Lambda 빌드의 경우, corelocal 의존성 설치를 별개의 레이어로 분리한 뒤, 최소 실행 이미지에 필요한 파일만 복사합니다.
  • 위 패턴은 이미지를 작게 유지하고 캐시 효율성을 높이며, 프로덕션 준비가 된 상태에서 UV의 강력한 의존성 해결 기능을 활용할 수 있게 합니다.
# Use the Python 3.14 runtime for ARM64
FROM python:3.14-arm64 AS final

# Copy core dependencies from the virtual environment built earlier
COPY --from=builder /build/.venv/lib /var/lang/lib

# Copy shared/common files
COPY --from=builder /build/common /var/task

# Copy the application code
COPY --from=builder /build/app /var/task

# Set the command to your Lambda handler
CMD [ "ingestion_worker.main.handler" ]

이렇게 하면 Lambda가 런타임에 실제로 필요로 하는 것만 포함된 더 작고 깔끔한 최종 이미지가 생성됩니다.

Back to Blog

관련 글

더 보기 »

Claw로 휴대폰에서 Claude Code 제어하기

문제: 당신은 Claude Code 세션에 깊이 몰두해 있습니다. 복잡한 작업을 진행 중입니다. 하지만 잠시 떠나야 합니다—커피를 마시거나, 전화를 받거나, 아이를 데려와야 합니다. 무엇을…