파이썬 패키징 #2 pip와 requirements.txt: 전통 워크플로의 한계
앞 글에서 프로젝트마다 가상 환경을 만들었습니다. 이제 그 안에 무엇을 설치했는지 기록하고 재현하는 문제가 남습니다. 전통적인 답이 pip와 requirements.txt입니다. 십수 년간 파이썬 생태계를 지탱해 온 방식이고 지금도 어디서나 마주치므로 정확히 알아야 하지만, 동시에 이 방식의 구멍들이 pyproject.toml과 uv가 태어난 이유이기도 합니다. 이번 글은 그 양쪽을 다 다룹니다.
pip install이 하는 일 #
pip install requests를 실행하면 pip는 PyPI(Python Package Index)에서 패키지를 찾아 wheel(미리 빌드된 zip 형식 배포물)을 내려받고, 가상 환경의 site-packages에 풀어 놓습니다. 이때 requests가 의존하는 패키지들(urllib3, certifi 등)도 함께 설치됩니다. 여러분이 지정한 것이 직접 의존성, 따라온 것들이 전이 의존성입니다. 이 구분이 이번 글의 핵심 개념입니다.
버전은 지정자로 제어합니다.
requests==2.32.3 # 정확히 이 버전
requests>=2.31 # 이상
requests~=2.32.0 # 2.32.x 안에서 최신 (호환 릴리스)
requests # 무엇이든 최신pip list로 설치 목록을, pip show requests로 특정 패키지의 버전과 의존 관계를 확인합니다.
requirements.txt: 목록을 파일로 #
의존성을 파일에 적어 두면 환경을 다시 만들 수 있습니다.
# requirements.txt
django==5.2.4
requests~=2.32.0
celerypip install -r requirements.txt새 머신에서도, 동료의 컴퓨터에서도 이 두 줄이면 환경이 만들어집니다. 여기까지가 교과서의 그림이고, 실무의 그림은 지금부터입니다.
pip freeze의 함정: 스냅샷은 의도를 지웁니다 #
“지금 환경을 그대로 기록하고 싶다"에 대한 전통 답변은 freeze입니다.
pip freeze > requirements.txt현재 site-packages의 모든 패키지가 정확한 버전으로 출력됩니다. 재현성은 확보되지만, 대가가 있습니다.
amqp==5.3.1
billiard==4.2.1
celery==5.5.3
click==8.2.1
kombu==5.5.4
vine==5.1.0
...이 목록에서 내가 설치한 것이 무엇인지 알 수 없습니다. celery 하나를 넣었을 뿐인데 전이 의존성 다섯 개가 같은 자격으로 나란히 적힙니다. 시간이 지나면 세 가지 문제가 자랍니다.
- 삭제가 안 됩니다: celery를 걷어내도 amqp, kombu가 직접 의존성인지 잔재인지 파일만 봐서는 판단할 수 없어, 목록은 늘기만 합니다.
- 업그레이드가 무섭습니다: 어떤 줄이 의도(우리가 2.32이어야 함)이고 어떤 줄이 우연(그날 설치된 버전)인지 구분이 없습니다.
- 플랫폼이 박힙니다: macOS에서 freeze한 목록에는 macOS에서만 필요한 패키지가 섞여, 리눅스 CI에서 그대로 안 돌기도 합니다.
반대쪽 함정: 안 박으면 설치 시점 복권입니다 #
freeze가 싫다고 버전 없이 celery 한 줄만 적으면 반대쪽 구멍에 빠집니다. 설치하는 날짜에 따라 다른 환경이 나옵니다. 오늘 CI는 celery 5.5를 받고, 다음 달 신입의 노트북은 5.6을 받습니다. “제 컴퓨터에서는 되는데요"의 상당수가 이 시차입니다. 직접 의존성에 버전을 박아도 전이 의존성은 여전히 떠 있으므로 구멍이 완전히 닫히지 않습니다.
정리하면 requirements.txt 하나로는 “의도의 목록"과 “재현용 스냅샷"이라는 서로 다른 두 문서를 겸하게 되고, 어느 쪽을 택해도 반대쪽이 무너집니다. 전통 워크플로는 이를 파일 두 개(requirements.in에 의도, 컴파일된 requirements.txt에 스냅샷)로 갈라 풀었고(pip-tools), 이 “선언과 잠금의 분리"가 바로 현대 도구들의 표준 설계가 됩니다. #3의 pyproject.toml이 선언을, #5의 lock 파일이 잠금을 맡습니다.
constraints: 버전만 누르는 보조 파일 #
전통 워크플로에서 하나 더 알아 둘 도구가 constraints입니다.
pip install -r requirements.txt -c constraints.txtconstraints 파일은 설치를 시키지는 않고, 설치될 때의 버전만 제한합니다. “urllib3는 보안 이슈로 2.5 미만 금지” 같은 조직 차원의 규칙을 프로젝트별 requirements와 분리해 둘 때 씁니다. 전이 의존성의 버전을 누를 수 있는 유일한 전통 수단이기도 합니다.
그래도 requirements.txt를 만나는 곳 #
한계를 알았어도 이 형식은 사라지지 않습니다. 도커 베이스 이미지 빌드, 레거시 배포 스크립트, 일부 PaaS의 기본 감지, 협업 상대의 저장소에서 계속 마주칩니다. 읽고 진단할 수 있어야 하는 이유입니다. 다행히 다음 세대 도구는 이 형식과 양방향으로 호환됩니다(#4에서 uv가 requirements.txt를 그대로 흡수하는 것을 봅니다).
정리 #
이번 글에서 다룬 내용입니다.
- pip는 PyPI에서 wheel을 받아 site-packages에 풀고, 지정한 직접 의존성과 따라오는 전이 의존성이 함께 설치됩니다
- 버전 지정자는
==(고정),>=(이상),~=(호환 릴리스)를 읽을 수 있으면 됩니다 pip freeze는 재현성을 주는 대신 의도를 지웁니다. 직접·전이 의존성이 구분 없이 섞여 삭제와 업그레이드가 어려워집니다- 버전을 안 박으면 설치 시점마다 다른 환경이 나옵니다. 직접 의존성만 박아도 전이 의존성이 떠 있어 구멍은 남습니다
- 근본 원인은 파일 하나가 “의도"와 “스냅샷"을 겸하는 것입니다. 선언과 잠금의 분리가 현대 도구의 표준 설계입니다
- constraints 파일은 설치 없이 버전만 제한하는 보조 수단입니다
다음 글(#3 pyproject.toml)에서는 “의도의 목록"이 살게 될 표준 거처, pyproject.toml을 다룹니다. 의존성 선언부터 도구 설정까지 프로젝트의 모든 정보가 한 파일로 모이는 과정입니다.