파이썬 패키징 #1 가상 환경: 환경이 꼬이는 원리와 venv

5 분 소요

분명히 pip install을 했는데 ModuleNotFoundError가 나고, 프로젝트 A를 고치려고 패키지를 올렸더니 프로젝트 B가 깨지고, 인터넷에서 본 대로 sudo pip install을 했더니 이제는 OS 명령이 이상해집니다. 파이썬을 조금 쓰다 보면 누구나 한 번은 겪는 이 혼란은 실력 문제가 아니라 구조 문제입니다. 이 시리즈는 그 구조를 바닥부터 정리하는 7편입니다. 가상 환경과 pip의 원리에서 시작해 pyproject.toml, uv, 의존성 잠금, PyPI 배포, 팀 규약까지 올라갑니다. 모던 파이썬 기초를 마친 분 기준이며, 테스트·자동화 시리즈에서 당연하게 쓰던 uv가 어떤 문제를 푸는 도구인지도 이 시리즈에서 밝혀집니다.

import는 어디를 뒤지는가: sys.path와 site-packages #

import requests가 실행되면 파이썬은 sys.path에 나열된 디렉터리를 순서대로 뒤집니다. 직접 확인할 수 있습니다.

sys.path 확인
python -c "import sys; print('\n'.join(sys.path))"

목록의 끝쪽에 .../site-packages가 보입니다. pip가 패키지를 설치하는 곳이 바로 이 디렉터리이고, 인터프리터 하나에 site-packages도 하나입니다. 문제는 여기서 시작됩니다. 컴퓨터의 모든 프로젝트가 인터프리터 하나를 같이 쓰면, site-packages도 하나를 같이 씁니다.

프로젝트 A가 django==4.2를 쓰고 프로젝트 B가 django==5.2를 쓴다면, 같은 site-packages에 두 버전을 나란히 둘 방법이 없습니다. B를 위해 올리면 A가 깨집니다. “며칠 전까지 되던 프로젝트가 안 된다"의 정체는 대부분 이것입니다. 다른 프로젝트를 만지다가 공유 창고의 내용물을 바꾼 것입니다.

시스템 파이썬은 OS의 부품입니다 #

macOS와 리눅스에는 파이썬이 이미 설치되어 있습니다. 이것은 여러분을 위한 파이썬이 아니라 OS 도구들이 쓰는 부품입니다. 여기의 site-packages를 sudo pip install로 바꾸면 패키지 관리자(apt, brew)가 관리하던 파일과 충돌하고, 최악의 경우 OS 유틸리티가 죽습니다.

그래서 최근 배포판은 아예 막아 둡니다. 데비안·우분투 계열에서 시스템 파이썬에 pip 설치를 시도하면 이 에러를 만납니다.

출력 예시
error: externally-managed-environment
× This environment is externally managed

PEP 668이 정의한 보호 장치로, “이 파이썬은 OS 것이니 가상 환경을 만들어 쓰라"는 뜻입니다. 에러를 우회하는 플래그(--break-system-packages)가 있지만, 이름 그대로 시스템을 부수겠다는 서명이므로 쓰지 않습니다. 정답은 다음 절입니다.

venv: 프로젝트마다 격리된 파이썬 #

가상 환경은 프로젝트 전용 site-packages를 가진 가벼운 파이썬 복사본입니다. 표준 라이브러리 venv 모듈로 만듭니다.

가상 환경 생성
cd my-project
python -m venv .venv

.venv 디렉터리가 생기고, 안을 들여다보면 구조가 단순합니다.

폴더 구조
.venv/
├── bin/            # python, pip 실행 파일 (윈도우는 Scripts/)
├── lib/
│   └── python3.13/
│       └── site-packages/   # 이 프로젝트 전용 창고
└── pyvenv.cfg      # 원본 인터프리터 위치 기록

인터프리터 전체를 복사하는 것이 아니라 원본을 가리키는 얇은 껍데기에 빈 site-packages를 새로 붙인 것입니다. 그래서 생성이 빠르고 용량 부담도 설치한 패키지만큼만 늘어납니다. 이제 프로젝트 A와 B는 각자의 .venv를 가지므로 django 4와 5가 공존합니다. 충돌의 구조적 원인이 사라진 것입니다.

activate의 정체: PATH 조작일 뿐입니다 #

가상 환경을 “켠다"고 표현하지만, 실제로 일어나는 일은 소박합니다.

활성화
source .venv/bin/activate
(.venv) $ which python
/Users/me/my-project/.venv/bin/python

activate 스크립트는 셸의 PATH 맨 앞에 .venv/bin을 끼워 넣는 것이 하는 일의 전부입니다. 이후 python, pip를 치면 가상 환경 쪽 실행 파일이 먼저 잡힐 뿐, 마법 같은 전환은 없습니다. deactivate는 PATH를 원래대로 되돌립니다.

이 정체를 알면 두 가지가 따라옵니다. 첫째, activate 없이도 경로를 직접 지정하면 가상 환경이 그대로 쓰입니다.

직접 실행
.venv/bin/python main.py        # activate 없이 실행
.venv/bin/pip install requests  # activate 없이 설치

cron이나 systemd처럼 셸 초기화가 없는 곳에서는 이 방식이 표준입니다. 둘째, “activate를 깜빡했다"는 사고는 which python 한 줄로 진단됩니다. 지금 잡히는 python이 어느 경로인지가 모든 환경 문제 디버깅의 첫 질문입니다.

지켜야 할 규칙 세 가지 #

  1. 프로젝트마다 .venv 하나: 위치는 프로젝트 루트의 .venv가 사실상 표준입니다. 도구들(VS Code, uv)이 이 이름을 자동 인식합니다.
  2. .venv는 커밋하지 않습니다: .gitignore에 넣습니다. 가상 환경은 절대 경로가 박혀 있어 다른 머신에서 재사용할 수 없고, 재현은 다음 편에서 다룰 의존성 기록으로 합니다.
  3. 시스템 파이썬에는 아무것도 설치하지 않습니다: 전역에 두고 싶은 도구가 있어도 방법이 따로 있습니다(#4에서 다룰 uv tool이 그 답입니다).

정리 #

이번 글에서 다룬 내용입니다.

  • importsys.path를 순서대로 뒤지고, pip는 site-packages에 설치합니다. 인터프리터 하나에 site-packages가 하나라서 전역 설치는 프로젝트끼리의 충돌을 만듭니다
  • 시스템 파이썬은 OS의 부품입니다. externally-managed-environment 에러(PEP 668)는 그 보호 장치이고, 우회하지 않습니다
  • venv는 프로젝트 전용 site-packages를 가진 얇은 파이썬 껍데기입니다. python -m venv .venv로 만들고, 프로젝트마다 하나씩 둡니다
  • activate는 PATH 맨 앞에 .venv/bin을 끼우는 것이 전부입니다. .venv/bin/python을 직접 불러도 되고, 환경이 이상하면 which python부터 확인합니다
  • .venv는 커밋하지 않습니다. 재현은 의존성 기록으로 합니다

다음 글(#2 pip와 requirements.txt)에서는 그 의존성 기록의 전통적 방법인 pip와 requirements.txt를 다룹니다. 잘 쓰는 법과 함께, 이 방식이 어디서 한계에 부딪히는지가 이후 편들을 이해하는 열쇠가 됩니다.

X