Python 개발 환경
가상환경이 왜 필요한지부터 venv, pip, PyPI의 역할, conda와 uv 비교까지 정리한 내용입니다.
가상환경이 왜 필요한지부터 venv, pip, PyPI의 역할, 그리고 conda와 uv 비교까지 정리한 노트입니다.
Python의 가상환경
Python 패키지를 시스템에 바로 설치하면 아래와 같은 문제가 생길 수 있습니다.
프로젝트 간 충돌: A 프로젝트는 Python 3.10을, B 프로젝트는 Python 3.12를 쓴다고 해봅시다. Python 버전마다 호환되는 라이브러리 버전이 다르기 때문에, 인터프리터가 하나뿐이면 두 프로젝트를 모두 감당할 수 없습니다. 실제로 TensorFlow 2.15(2023년 말)는 Python 3.9~3.11까지만 지원했고, 3.12 지원은 TF 2.16에서야 들어왔습니다.
시스템 오염: OS 자체가 Python의 사용자입니다. Ubuntu의
add-apt-repository같은 시스템 도구들이/usr/bin/python3와 그 위에 깔린 라이브러리로 동작합니다. 시스템 Python에 패키지를 직접 설치하면 이 라이브러리들이 다른 버전으로 덮어써질 수 있고, 최악의 경우 패키지 관리자 자체가 깨져서 복구할 도구마저 잃게 됩니다.
이런 문제를 해결하기 위해 도입된 것이 가상환경(Virtual Environment)입니다. 가상환경은 프로젝트마다 격리된 실행 환경을 만들어, 인터프리터와 패키지를 환경별로 따로 관리합니다. A 가상환경은 Python 3.10을, B 가상환경은 Python 3.12를 가리키게 하면 두 프로젝트가 한 시스템에서 충돌 없이 공존합니다.
Python Packaging의 여러 역할
Python Packaging은 여러 역할로 나누어져 있습니다. 아래는 이 포스트에서 다룰 역할입니다.
| 역할 | 담당 부품 | 하는 일 |
|---|---|---|
| 언어 | Python 인터프리터, 표준 라이브러리 | 코드를 실행한다 |
| 격리 | venv | 프로젝트마다 독립된 환경을 만든다 |
| 설치 | pip | 패키지를 받아 환경에 설치한다 |
| 저장소 | PyPI | 패키지를 올리고 내려받는 공식 저장소 |
각 역할을 하나씩 확인해봅시다.
언어:
python3 main.py를 실행하면 인터프리터인python3가main.py를 읽어 실행합니다.python3의 실체는 디스크에 설치된 실행 파일이고,which python3로 경로를 확인할 수 있습니다.import json이 설치 없이 되는 건 표준 라이브러리가 인터프리터와 함께 깔리기 때문입니다.격리: venv는 Python에 내장된 가상환경 패키지로, 프로젝트마다 독립된 실행 환경(
.venv/)을 만듭니다. 내장이라는 건 직접 확인할 수 있습니다. venv 모듈의 위치를 물어보면 표준 라이브러리 경로가 나옵니다.1 2
python3 -c "import venv; print(venv.__file__)" # .../lib/python3.12/venv/__init__.py
가상환경을 만드는 명령은 네 부분으로 읽습니다.
1 2
python3 -m venv .venv ① ② ③ ④
① 인터프리터를 실행해서 ②
-m으로 모듈을 지정해 실행하는데 ③ 그 모듈이 venv이고 ④.venv는 가상환경을 저장할 폴더 이름입니다.실행하면 프로젝트 안에 이런 구조가 생깁니다.
1 2 3 4 5 6 7
.venv/ ├── bin/ # 이 환경 전용 python, pip, activate ├── include/ ├── lib/ │ └── python3.12/ │ └── site-packages/ # 이 환경에 설치되는 패키지가 쌓이는 곳 └── pyvenv.cfg # 어느 인터프리터 기반인지 기록
이후 어느 가상환경을 활성화(
source .venv/bin/activate)하느냐에 따라python이 가리키는 인터프리터가 바뀝니다.1 2 3 4
source projectA/.venv/bin/activate python --version # Python 3.10.14 source projectB/.venv/bin/activate python --version # Python 3.12.4
venv는 격리만 담당합니다. 패키지 설치는 pip(바로 다음 역할), Python 버전 관리는 pyenv 같은 별도 도구가 맡습니다.
설치: 가상환경이 활성화된 상태에서
pip install pandas를 실행하면, 시스템이 아니라 그 환경 폴더(.venv/lib/python3.12/site-packages/)에 설치됩니다. pip는 “Pip Installs Packages”의 재귀 약자로, Python에 기본 동봉되는 표준 설치기입니다.1 2 3 4 5 6
pip install pandas # PyPI에서 받아 현재 환경에 설치 pip install "pandas==2.2.0" # 버전 지정 pip uninstall pandas # 제거 pip list # 설치된 패키지 목록 pip freeze > requirements.txt # 현재 환경을 파일로 기록 pip install -r requirements.txt # 그 파일대로 재설치
마지막 두 줄에 나오는
requirements.txt는 현재 환경에 깔린 패키지 목록을 버전과 함께 한 줄씩 적은 텍스트 파일입니다. 이 파일을 Git에 올려두면 다른 머신에서-r옵션 한 번으로 같은 환경을 재현할 수 있습니다. 다만pip freeze는 그 순간 환경에 깔려 있는 모든 패키지를 그대로 받아 적는 스냅샷이라, 내가 원해서 설치한 패키지(pandas)와 딸려 온 하위 의존성(dependency, numpy 등)이 구분 없이 똑같은 자격으로 적힙니다.여기서 중요한 것은 pip는 “어느 환경에 설치하는가”를 모른다는 점입니다. 그저 자기가 묶인 Python에 설치할 뿐이라, 활성화를 깜빡하면 엉뚱한 환경에 깔리고 “설치했는데 import가 안 돼요” 상황이 생깁니다. requirements.txt 사용의 문제와 이 “pip install” 문제를 함께 해결한 것이 뒤에서 다룰 uv입니다.
저장소: 3번에서 pip가 pandas를 받아온 곳이 PyPI(Python Package Index)입니다. 설치 로그에 찍히는
Downloading pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl이 PyPI에서 내려받은 파일입니다. PyPI의 패키지는 두 형태로 올라옵니다.- wheel: 미리 빌드된 완성품. 받아서 풀면 설치 끝.
- sdist(소스 배포): 소스 코드. 내 컴퓨터에서 컴파일해야 함.
pip install이 유독 오래 걸리면서 “building wheel…“이 뜬다면, 내 환경(OS와 아키텍처)에 맞는 wheel이 없어서 소스를 빌드하는 중입니다. C 컴파일러가 없으면 여기서 에러가 납니다. 반대로 요즘 NumPy나 torch 같은 무거운 라이브러리가 순식간에 설치되는 것은 거의 모든 환경의 wheel을 미리 만들어 올려두기 때문입니다.
Python 생태계의 표준은 PEP(Python Enhancement Proposal)라는 공식 문서로 정해집니다. 패키지를 어떤 형식으로 만들고(wheel), 프로젝트 의존성을 어떻게 선언하는지(pyproject.toml)도 각각 PEP로 정의되어 있습니다. 이 규칙들이 누구나 읽을 수 있는 공식 문서로 존재하기 때문에, 누구든 표준만 따르면 새 도구를 만들 수 있습니다. Astral 같은 제3자가 그 문서만 보고 uv를 만들어도 기존 생태계와 완벽히 호환되는 이유입니다.
이 표준 생태계 밖에 있는 유일한 주요 도구가 conda입니다.
conda
conda는 Anaconda 배포판과 함께 퍼진 패키지 및 환경 매니저입니다. venv와 달리 가상환경, 패키지 설치, Python 버전 관리를 전부 할 수 있습니다. 여담으로 제가 대학교 신입생이던 시절에는 conda로 수업을 진행했습니다. 트렌드가 바뀌어 요즘 신입생들은 표준 Python으로 수업을 진행하는 것으로 알고 있습니다.
1
2
3
conda create -n myenv python=3.11
conda activate myenv
conda install numpy
가장 큰 특징은 PyPI의 표준 생태계 밖에 있다는 점입니다. 저장소는 PyPI 대신 자체 채널을 쓰고, 패키지 포맷도 자체 포맷을 사용합니다. 애초에 회사(Anaconda)에서 출발한 도구라 Python 표준 없이 설계됐고, 덕분에 CUDA, MKL 같은 C 라이브러리까지 함께 관리할 수 있었습니다.
현재는 PyPI wheel이 바이너리를 다 내장하면서 conda만의 장점은 줄었습니다. 이에 더해 느린 의존성 해석과 무거운 설치 용량이라는 단점으로 많이 쓰이지 않게 되었습니다.
uv
uv는 Astral이 Rust로 만든 패키지 및 프로젝트 매니저입니다. conda와 달리 표준 생태계 안의 구현체라서, 같은 PyPI와 wheel과 pyproject.toml을 씁니다.
| 기존 도구 | 역할 | uv에서는 |
|---|---|---|
| pyenv | Python 버전 설치와 전환 | uv python install / pin |
| venv | 가상환경 생성 | uv venv (또는 uv sync가 자동 생성) |
| pip | 패키지 설치 | uv add / uv pip install |
| pip-tools | 의존성 잠금(lock) | uv lock (자동) |
1
2
3
4
5
6
7
brew install uv
uv init # 새 프로젝트 (pyproject.toml 생성)
uv python pin 3.11 # Python 버전 고정 (.python-version)
uv add pandas lightgbm # 의존성 추가 (pyproject + lock 갱신 + 설치)
uv sync # pyproject/lock 기준으로 .venv 동기화
uv run python main.py # activate 없이 바로 실행
위 포스트에서 이야기 했던 pip의 두 문제를 uv는 아래와 같이 해결합니다.
requirements.txt의 스냅샷 문제는 파일을 두 개로 분리해서 해결합니다. uv add pandas를 실행하면 pyproject.toml에는 내가 원한 pandas 한 줄만 적힙니다. 딸려 온 numpy를 포함한 전체 의존성 트리는 uv가 계산해서 uv.lock에 정확한 버전으로 자동 기록합니다.
“어느 환경에 설치하는가” 문제는 설치 대상을 정하는 기준을 바꿔서 해결합니다. pip는 자기가 묶인 Python에 설치하지만, uv는 현재 폴더에서 pyproject.toml을 찾아 그 프로젝트의 .venv에 설치하고(uv add) 그 .venv로 실행합니다(uv run). 따라서 activate 여부와 무관하게 항상 프로젝트의 환경을 대상으로 작동합니다.
pyproject.toml 읽는 법
요즘 Python 프로젝트의 표준 명세 파일입니다. 최소 형태는 이렇게 생겼습니다.
1
2
3
4
5
6
7
[project]
name = "ml-study"
requires-python = ">=3.11"
dependencies = [
"pandas>=2.2,<3",
"uvicorn[standard]>=0.30",
]
여기 나오는 버전 지정 문법은 세 개만 알면 됩니다.
1
2
3
pandas==2.2.0 # 정확히 이 버전
pandas>=2.2,<3 # 범위 (메이저 버전에 상한을 두는 게 관례)
pandas~=2.2.0 # 2.2.x는 허용, 2.3은 불허 (compatible release)
uvicorn[standard]처럼 대괄호가 붙은 것은 extras라는 정식 문법입니다. uvicorn 본체만 설치하지 말고, uvicorn이 “standard”라는 이름으로 묶어둔 추가 패키지들까지 같이 설치하라는 뜻입니다.
세 도구 비교
| venv (+pip) | conda | uv | |
|---|---|---|---|
| 가상환경(격리) | O (이것만 함) | O | O |
| 패키지 설치 | X (pip 별도) | O | O |
| Python 버전 관리 | X (pyenv 별도) | O | O |
| 의존성 잠금 | X (수동 관리) | 부분적 (environment.yml) | O (uv.lock 자동) |
| 패키지 출처 | PyPI | conda 채널 | PyPI |
| 속도 | 보통 | 느림 | 매우 빠름 |
결론: 이 장황한 공부의 끝은, “26년 기준 uv를 쓰는 것이 제일 좋다”입니다.