파이썬 가상환경과 패키지 관리 - venv pip requirements (파이썬 중급 14단원)
이 단원에서 배우는 것
13단원까지 주문 로그 분석 도구의 알맹이를 만들었다. 여기까지는 표준 라이브러리만 썼기 때문에 파이썬만 있으면 어디서든 돌아갔다. 이제 이 도구를 팀에 넘기고 서버에 올려야 한다. 엑셀 출력이 필요해서 외부 패키지를 하나 깔면, 그 순간부터 "내 컴퓨터에서는 되는데"가 시작된다. 이번 단원은 코드가 아니라 코드가 돌아갈 환경을 다룬다.
- 가상환경이 정확히 무엇을 격리하는지 알고
venv로 프로젝트별 환경을 만들 수 있다 pip freeze를 그대로requirements.txt로 쓰면 왜 나중에 문제가 되는지 설명할 수 있다- 직접 쓴 의존성과 딸려 온 의존성을 분리해 재현 가능한 설치를 만들 수 있다
왜 필요한가
파이썬은 기본적으로 패키지를 파이썬 설치 위치 한곳에 깐다. 프로젝트가 하나면 문제가 없다. 두 개가 되는 순간 이런 일이 생긴다.
- 주문 분석 도구는
pandas 1.5에서 돌아가는데, 옆 프로젝트가pandas 2.2를 요구한다. 한 컴퓨터에 두 버전을 동시에 둘 수 없으므로pip install한 번으로 다른 프로젝트가 깨진다. - 맥에서 개발하고 리눅스 서버에 올렸는데 임포트 에러가 난다. 로컬에 깔린 패키지 중 무엇이 이 프로젝트 때문에 깔린 것인지 아무도 모른다.
- 운영체제가 쓰는 파이썬에 패키지를 덮어써서 시스템 도구가 동작을 멈춘다. 최근 배포판에서
pip install이externally-managed-environment오류로 거절하는 이유가 이것이다.
가상환경은 프로젝트마다 패키지 설치 위치를 따로 두는 것이다. 그 이상도 이하도 아니다. 파이썬 인터프리터 자체를 복사하는 게 아니라, 그 인터프리터를 가리키면서 site-packages만 자기 폴더에 두는 얇은 껍데기를 만든다.
문법과 예제
venv 만들고 쓰기
venv는 파이썬 3.3부터 표준 라이브러리에 들어 있다. 따로 설치할 것이 없다.
# 프로젝트 폴더에서
python3 -m venv .venv
# 활성화 (macOS / Linux)
source .venv/bin/activate
# 활성화 (Windows PowerShell)
.venv\Scripts\Activate.ps1
# 확인
which python # /path/to/project/.venv/bin/python
python -V # Python 3.11.x
pip -V # pip 23.x from /path/to/project/.venv/lib/python3.11/site-packages/pip
# 빠져나오기
deactivate
활성화가 하는 일은 PATH 앞에 .venv/bin을 끼워 넣는 것뿐이다. 그래서 python이라고 치면 가상환경 쪽이 먼저 잡힌다. 마법이 아니라는 것을 알아 두면 문제가 생겼을 때 which python 한 줄로 원인을 찾을 수 있다.
사실 활성화는 필수가 아니다. 아래처럼 직접 지목해도 똑같이 동작하고, 서버 스크립트나 cron에서는 이쪽이 더 안전하다.
.venv/bin/python -m pip install requests
.venv/bin/python -m analyze_orders
폴더 이름은 .venv가 사실상 표준이다. 편집기들이 이 이름을 자동으로 찾는다. 그리고 .venv는 반드시 .gitignore에 넣는다. 안에는 절대 경로가 박힌 파일이 있어서 다른 컴퓨터로 옮기면 동작하지 않는다. 옮길 것은 환경이 아니라 환경을 만드는 방법이다.
pip 로 설치하기
python -m pip install requests # 최신 버전
python -m pip install "requests==2.31.0" # 정확한 버전 고정
python -m pip install "requests>=2.31,<3" # 범위 지정
python -m pip install -e . # 현재 프로젝트를 편집 가능 모드로
python -m pip list # 설치된 것 보기
python -m pip show requests # 어떤 패키지가 이것을 요구하는지 확인
python -m pip uninstall requests
pip install이 아니라 python -m pip install을 권하는 이유가 있다. pip라는 명령이 어느 파이썬에 붙어 있는지는 PATH에 달렸고, 가상환경을 켰다고 생각했는데 시스템 pip가 잡히는 사고가 실제로 자주 난다. python -m pip는 지금 이 python에 설치한다는 뜻이라 헷갈릴 여지가 없다.
버전 범위에서 <3처럼 상한을 두는 이유는 메이저 버전이 올라갈 때 호환이 깨지는 관행 때문이다. 상한을 안 두면 몇 달 뒤 CI가 갑자기 빨갛게 된다.
requirements: 두 개의 파일로 나눈다
가장 흔한 방식은 이것이다.
python -m pip freeze > requirements.txt
편하지만 결과물이 이렇게 된다.
certifi==2024.7.4
charset-normalizer==3.3.2
idna==3.7
openpyxl==3.1.5
et-xmlfile==1.1.0
requests==2.32.3
urllib3==2.2.2
여기서 내가 직접 쓰기로 결정한 것은 openpyxl과 requests 두 개고, 나머지 다섯은 그 둘이 데려온 것이다. 파일만 봐서는 구분이 안 된다. 여섯 달 뒤 requests를 안 쓰게 됐을 때 certifi, idna, urllib3을 같이 지워도 되는지 판단할 수 없어서, 결국 아무도 못 지우고 목록이 계속 자란다. 이것이 흔히 말하는 의존성 지옥의 시작이다.
분리하면 이 문제가 사라진다. 직접 쓰는 것만 손으로 적은 파일을 따로 둔다.
# requirements.in — 사람이 관리한다. 직접 임포트하는 것만.
requests>=2.31,<3
openpyxl>=3.1,<4
# requirements-dev.in — 개발할 때만 필요한 것
-r requirements.in
pytest>=8.0
mypy>=1.8
여기서 잠글 파일을 생성한다. pip-tools나 uv가 이 일을 한다.
python -m pip install pip-tools
pip-compile requirements.in # requirements.txt 생성 (전이 의존성 포함, 버전 고정)
pip-sync requirements.txt # 목록과 정확히 일치하도록 설치/삭제
생성된 requirements.txt에는 각 줄마다 "무엇 때문에 깔렸는지" 주석이 붙는다. 사람은 .in만 고치고 .txt는 명령으로 다시 만든다. 이 규칙 하나로 "지워도 되는가" 문제가 없어진다. 개발 도구를 -dev로 나누는 것도 중요하다. 운영 서버에 pytest와 mypy가 깔려 있을 이유가 없다.
pyproject.toml: 도구가 아니라 프로젝트를 기술한다
이 분석 도구를 다른 프로젝트에서 임포트해 쓸 거라면 패키지로 만든다. 요즘 표준은 setup.py가 아니라 pyproject.toml이다.
[project]
name = "order-analyzer"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"requests>=2.31,<3",
"openpyxl>=3.1,<4",
]
[project.optional-dependencies]
dev = ["pytest>=8.0", "mypy>=1.8"]
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
python -m pip install -e ".[dev]"
-e는 편집 가능 설치다. 소스를 고치면 재설치 없이 바로 반영된다. 이렇게 해 두면 sys.path를 손대거나 상대 임포트 때문에 씨름할 일이 없어진다. 테스트에서 ModuleNotFoundError가 나는 문제의 상당수가 이 한 줄로 끝난다.
재현성을 올리는 두 가지
# 1. 해시 검증: 잠근 버전이 실제로 그 파일인지까지 확인한다
pip-compile --generate-hashes requirements.in
python -m pip install --require-hashes -r requirements.txt
# 2. 인터넷 없이 재현: 미리 받아 두고 그것만 쓴다
python -m pip download -r requirements.txt -d wheels/
python -m pip install --no-index --find-links wheels/ -r requirements.txt
운영 배포에서는 --require-hashes를 켜는 것이 좋다. 같은 버전 번호로 다른 내용이 배포되는 사고를 막는다.
실무에서 자주 틀리는 것
1. pip freeze 결과를 그대로 커밋한다
앞에서 본 "직접 의존성과 전이 의존성이 섞인다" 문제 외에 하나가 더 있다. pip freeze는 지금 이 운영체제, 이 파이썬 버전에서 깔린 것을 그대로 뱉는다. 맥에서 뽑은 목록을 리눅스 서버에 그대로 쓰면 그쪽에 없는 패키지가 끼어 있어 설치가 실패한다. 게다가 로컬에서 잠깐 시험해 본 패키지, 편집기가 요구해서 깔린 패키지까지 전부 들어간다. pip freeze는 지금 상태를 확인할 때 쓰는 명령이지 의존성을 선언하는 명령이 아니다.
2. sudo pip install 을 한다
sudo pip install pandas # 시스템 파이썬을 덮어쓴다
권한 오류가 나면 반사적으로 sudo를 붙이게 되는데, 이건 운영체제가 관리하는 패키지를 파이썬 패키지로 갈아 끼우는 행위다. 리눅스에서는 apt가 설치한 라이브러리 버전이 바뀌면서 시스템 도구가 망가진다. 최근 배포판이 externally-managed-environment 오류로 이걸 막는 것도 그래서다. 권한 오류가 났다는 것은 대개 가상환경을 안 켰다는 신호다. sudo가 아니라 source .venv/bin/activate가 답이다.
3. .venv 폴더를 커밋하거나 복사해 옮긴다
cat .venv/bin/activate | head -5
# VIRTUAL_ENV="/Users/hong/projects/order-analyzer/.venv" <- 절대 경로가 박혀 있다
폴더를 옮기거나 이름을 바꾸면 그 안의 스크립트들이 옛 경로를 가리켜 깨진다. 저장소에 넣으면 용량도 커지고, 다른 운영체제에서는 바이너리가 아예 안 돈다. .venv는 언제든 지우고 다시 만들 수 있는 캐시로 취급한다. 뭔가 꼬였을 때 지우고 다시 만드는 것이 가장 빠른 해결책인 경우가 많다.
4. 파이썬 버전 자체가 다르다는 것을 놓친다
python3 -m venv .venv # 이 python3 이 3.9 일 수도 있다
venv는 패키지를 격리할 뿐 파이썬 버전을 바꾸지 못한다. python3이 3.9를 가리키는 컴퓨터에서 만들면 3.9 환경이 생기고, 10단원에서 쓴 except*나 add_note 같은 3.11 문법이 그 컴퓨터에서만 문법 오류를 낸다. 만들 때 버전을 지목하고, pyproject.toml에 requires-python을 적어 둔다.
python3.11 -m venv .venv
여러 파이썬 버전을 오가야 한다면 pyenv나 uv python install 같은 도구로 버전 자체를 관리한다. 도구 선택이 고민이면 파이썬 패키징 가이드의 도구 권장 문서가 현재 시점의 기준을 알려 준다.
스스로 확인하기
- 동료가
requirements.txt에 있는urllib3을 지워도 되는지 물었다. 지금 파일이pip freeze로 만든 것이라면 왜 답하기 어려우며, 어떤 구조로 바꾸면 답할 수 있는가? pip install에서 권한 오류가 났다.sudo를 붙이면 왜 안 되고 무엇을 확인해야 하는가?- 가상환경을 켰는데도
pip install한 패키지가 임포트되지 않는다. 원인을 좁히기 위해 어떤 명령을 순서대로 실행하겠는가?
정답
pip freeze결과에는 직접 쓰기로 결정한 패키지와 그것들이 데려온 전이 의존성이 구분 없이 섞여 있다.urllib3이 내 코드가 임포트하는 것인지requests가 요구하는 것인지 파일만 봐서는 알 수 없다. 직접 의존성만requirements.in에 적고pip-compile로requirements.txt를 생성하면,.in에 있는지만 보면 답이 나온다.sudo pip는 운영체제가 관리하는 파이썬 패키지를 덮어써서 시스템 도구를 망가뜨릴 수 있고, 최근 배포판은externally-managed-environment오류로 이를 막는다. 권한 오류는 대개 가상환경이 켜지지 않아 시스템 경로에 설치를 시도한다는 신호이므로,which python과pip -V로 어느 파이썬이 잡혔는지 먼저 확인한다.which python과python -V로 실제 실행되는 인터프리터를 확인하고,pip -V로 그 pip이 같은 가상환경 것인지 본다. 다르면python -m pip install로 다시 설치한다. 같은데도 안 되면python -m pip show 패키지명으로 설치 위치를 확인하고,python -c "import sys; print(sys.path)"로 그 경로가 검색 대상에 있는지 본다.