Skip to content

워크스페이스 활용하기

Cargo에서 영감을 받은 워크스페이스는 "함께 관리되는 하나 이상의 패키지, 즉 _워크스페이스 멤버_의 집합"이다.

워크스페이스는 대규모 코드베이스를 공통 의존성을 가진 여러 패키지로 나누어 구성한다. 예를 들어, FastAPI 기반 웹 애플리케이션과 함께 별도의 Python 패키지로 버전 관리되고 유지되는 일련의 라이브러리를 동일한 Git 저장소에서 관리할 수 있다.

워크스페이스에서 각 패키지는 자신만의 pyproject.toml을 정의하지만, 워크스페이스는 단일 lockfile을 공유한다. 이를 통해 워크스페이스가 일관된 의존성 세트로 동작하도록 보장한다.

따라서 uv lock은 전체 워크스페이스에 대해 한 번에 동작한다. 반면 uv runuv sync은 기본적으로 워크스페이스 루트에서 동작하지만, 두 명령 모두 --package 인자를 받아 특정 워크스페이스 멤버에 대해 명령을 실행할 수 있다. 이를 통해 워크스페이스 디렉터리 어디에서나 특정 멤버에 대한 작업을 수행할 수 있다.

시작하기

작업 공간을 생성하려면 pyproject.toml 파일에 tool.uv.workspace 테이블을 추가한다. 이렇게 하면 해당 패키지를 루트로 하는 작업 공간이 암시적으로 생성된다.

Tip

기존 패키지 내에서 uv init을 실행하면 새로 생성된 멤버가 작업 공간에 자동으로 추가된다. 작업 공간 루트에 tool.uv.workspace 테이블이 없으면 새로 생성한다.

작업 공간을 정의할 때는 members (필수)와 exclude (선택) 키를 지정해야 한다. 이 키들은 작업 공간에 포함하거나 제외할 특정 디렉터리를 각각 지정하며, glob 패턴 리스트를 받는다:

pyproject.toml
[project]
name = "albatross"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["bird-feeder", "tqdm>=4,<5"]

[tool.uv.sources]
bird-feeder = { workspace = true }

[tool.uv.workspace]
members = ["packages/*"]
exclude = ["packages/seeds"]

members glob 패턴에 포함되고 exclude glob 패턴에 제외되지 않은 모든 디렉터리는 pyproject.toml 파일을 포함해야 한다. 작업 공간 멤버는 애플리케이션 또는 라이브러리 중 하나일 수 있으며, 두 유형 모두 작업 공간에서 지원된다.

모든 작업 공간에는 루트가 필요하며, 이 루트도 작업 공간 멤버이다. 위 예제에서 albatross는 작업 공간 루트이며, packages 디렉터리 아래의 모든 프로젝트가 작업 공간 멤버가 된다. 단, seeds는 예외이다.

기본적으로 uv runuv sync는 작업 공간 루트에서 동작한다. 위 예제에서 uv runuv run --package albatross는 동일하며, uv run --package bird-feederbird-feeder 패키지에서 명령을 실행한다.

워크스페이스 소스

워크스페이스 내에서 멤버 간의 의존성은 tool.uv.sources를 통해 관리된다. 예를 들어:

pyproject.toml
[project]
name = "albatross"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["bird-feeder", "tqdm>=4,<5"]

[tool.uv.sources]
bird-feeder = { workspace = true }

[tool.uv.workspace]
members = ["packages/*"]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

이 예제에서 albatross 프로젝트는 워크스페이스의 멤버인 bird-feeder 프로젝트에 의존한다. tool.uv.sources 테이블의 workspace = true 키-값 쌍은 bird-feeder 의존성이 PyPI나 다른 레지스트리에서 가져오는 대신 워크스페이스에서 제공되어야 함을 나타낸다.

Note

워크스페이스 멤버 간의 의존성은 편집 가능하다.

워크스페이스 루트의 tool.uv.sources 정의는 특정 멤버의 tool.uv.sources에서 재정의하지 않는 한 모든 멤버에 적용된다. 예를 들어, 다음 pyproject.toml을 보자:

pyproject.toml
[project]
name = "albatross"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["bird-feeder", "tqdm>=4,<5"]

[tool.uv.sources]
bird-feeder = { workspace = true }
tqdm = { git = "https://github.com/tqdm/tqdm" }

[tool.uv.workspace]
members = ["packages/*"]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

이 경우, 모든 워크스페이스 멤버는 기본적으로 GitHub에서 tqdm을 설치한다. 단, 특정 멤버가 자체 tool.uv.sources 테이블에서 tqdm 항목을 재정의하지 않는 한 이 규칙이 적용된다.

워크스페이스 레이아웃

가장 일반적인 워크스페이스 레이아웃은 루트 프로젝트와 함께 여러 라이브러리가 있는 구조로 생각할 수 있다.

예를 들어, 위의 예제를 이어서 설명하면, 이 워크스페이스는 albatross를 명시적인 루트로 두고, packages 디렉토리 안에 두 개의 라이브러리(bird-feederseeds)가 있다:

albatross
├── packages
│   ├── bird-feeder
│   │   ├── pyproject.toml
│   │   └── src
│   │       └── bird_feeder
│   │           ├── __init__.py
│   │           └── foo.py
│   └── seeds
│       ├── pyproject.toml
│       └── src
│           └── seeds
│               ├── __init__.py
│               └── bar.py
├── pyproject.toml
├── README.md
├── uv.lock
└── src
    └── albatross
        └── main.py

seedspyproject.toml에서 제외되었기 때문에, 이 워크스페이스는 총 두 개의 멤버를 가진다: albatross(루트)와 bird-feeder이다.

워크스페이스 사용 시기(와 사용하지 말아야 할 때)

워크스페이스는 단일 저장소 내에서 여러 개의 상호 연결된 패키지를 개발하는 과정을 편리하게 만든다. 코드베이스가 복잡해지면, 독립적인 의존성과 버전 제약을 가진 작고 합성 가능한 패키지로 분할하는 것이 유용할 수 있다.

워크스페이스는 관심사의 분리와 격리를 강화한다. 예를 들어, uv에서는 코어 라이브러리와 커맨드라인 인터페이스를 별도의 패키지로 분리해 CLI와 독립적으로 코어 라이브러리를 테스트할 수 있다. 반대의 경우도 마찬가지다.

워크스페이스의 일반적인 사용 사례로는 다음과 같은 경우가 있다:

  • 확장 모듈(Rust, C++ 등)로 구현된 성능이 중요한 서브루틴을 가진 라이브러리
  • 플러그인 시스템을 가진 라이브러리. 각 플러그인은 루트에 의존성을 가진 별도의 워크스페이스 패키지다.

그러나 워크스페이스는 구성원 간에 충돌하는 요구사항이 있거나, 각 구성원이 별도의 가상 환경을 원하는 경우에는 적합하지 않다. 이런 경우에는 경로 의존성을 사용하는 것이 더 나은 선택이다. 예를 들어, albatross와 그 구성원들을 워크스페이스로 묶는 대신, 각 패키지를 독립적인 프로젝트로 정의하고 패키지 간 의존성을 tool.uv.sources에서 경로 의존성으로 정의할 수 있다:

pyproject.toml
[project]
name = "albatross"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["bird-feeder", "tqdm>=4,<5"]

[tool.uv.sources]
bird-feeder = { path = "packages/bird-feeder" }

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

이 접근 방식은 많은 동일한 이점을 제공하지만, 의존성 해결과 가상 환경 관리에 대해 더 세밀한 제어가 가능하다. 단점은 uv run --package를 더 이상 사용할 수 없고, 대신 관련 패키지 디렉토리에서 명령을 실행해야 한다는 점이다.

마지막으로, uv의 워크스페이스는 전체 워크스페이스에 대해 단일 requires-python을 강제하며, 모든 구성원의 requires-python 값의 교집합을 취한다. 워크스페이스의 다른 구성원이 지원하지 않는 Python 버전에서 특정 구성원을 테스트해야 한다면, uv pip를 사용해 별도의 가상 환경에 해당 구성원을 설치해야 할 수도 있다.

Note

Python은 의존성 격리를 제공하지 않기 때문에, uv는 패키지가 선언된 의존성만 사용하도록 보장할 수 없다. 특히 워크스페이스의 경우, uv는 패키지가 다른 워크스페이스 구성원이 선언한 의존성을 임포트하지 않도록 보장할 수 없다.