버전 고정과 lock 파일: 재현 가능한 빌드

==·>=·~= 같은 버전 지정자의 의미와, lock 파일이 전이 의존성까지 고정해 빌드를 재현 가능하게 만드는 원리를 실전 전략과 함께 완전 정복합니다.

· 7 min read · PALDYN Team

지난 글에서 패키지를 만들어 PyPI에 공개하는 과정을 마쳤다. 이제 쓰는 쪽으로 돌아와 이 시리즈를 마무리한다. 패키지를 설치할 때 우리는 requests라고만 적기도 하고 requests==2.31.0처럼 정확히 못 박기도 하는데, 이 선택이 “내 컴퓨터에서는 되던 게 배포 서버에서는 깨지는” 문제의 핵심이다. 버전 지정자의 의미와 lock 파일의 역할을 제대로 이해하면 이 불확실성을 없앨 수 있다.

버전 지정자가 뜻하는 범위

의존성에 붙이는 기호는 곧 “허용하는 버전의 범위”다. 넓게 열수록 자동 업데이트가 쉬워지지만, 그만큼 예기치 않게 동작이 바뀔 위험도 커진다.

버전 지정자가 뜻하는 범위

각 지정자의 의미를 정리하면 이렇다.

requests==2.31.0     정확히 그 버전만 (완전 고정)
requests>=2.31       2.31 이상 모두
requests~=2.31.0     2.31.* 호환 (>=2.31.0, <2.32.0)
requests>=2.0,<3.0   메이저 2 대역만
requests             아무 버전 — 재현성에 가장 위험

~=는 “호환 릴리스” 연산자로, 마지막 자리만 올라가는 패치 업데이트는 허용하되 그 위 자리는 고정한다. 시맨틱 버저닝을 따르는 패키지에서 “버그 수정은 받되 호환성 깨지는 변경은 막고 싶다”는 의도를 표현할 때 유용하다.

직접 의존성만으로는 부족하다

여기서 핵심적인 함정이 하나 있다. pyproject.toml이나 requirements.txt에 적는 건 보통 내가 직접 쓰는 패키지뿐이다. 그런데 그 패키지들은 또 다른 패키지에 의존한다(전이 의존성). 내가 requests만 적어도 실제로는 urllib3, certifi 등이 함께 깔리는데, 이들의 버전은 내 설정 어디에도 적혀 있지 않다.

그래서 같은 requirements.txt로 설치해도, 설치한 시점이 다르면 전이 의존성의 버전이 달라질 수 있다. 어제는 멀쩡했는데 오늘 새 환경에서 빌드가 깨지는 일이 이렇게 생긴다.

lock 파일이 채우는 빈자리

lock 파일은 바로 이 빈자리를 메운다. 의존성을 한 번 해결한 결과를, 전이 의존성까지 전부 포함해 정확한 버전으로 기록한다.

lock 파일과 재현 가능한 설치

poetry.lock, pdm.lock, uv.lock, 또는 pip-tools가 만드는 requirements.txt(컴파일된 형태)가 모두 이 역할을 한다. lock 파일에는 각 패키지의 정확한 버전은 물론, 무결성을 검증하는 해시까지 박혀 있다.

# lock 파일에는 전이 의존성까지 정확한 버전·해시가 박힌다
requests==2.31.0   --hash=sha256:...
urllib3==2.1.0     --hash=sha256:...
certifi==2023.11.17 --hash=sha256:...

이 파일을 기준으로 설치하면, 언제 어디서 설치하든 완전히 동일한 환경이 만들어진다. 재현 가능한 빌드의 토대다.

실전 전략: 두 층으로 나눈다

핵심 전략은 “원하는 것”과 “확정된 것”을 분리하는 것이다.

  • 선언(범위): pyproject.toml에는 사람이 의도한 범위(requests>=2.31)를 적는다. 읽기 쉽고, 의도가 드러난다.
  • 잠금(고정): lock 파일에는 그 범위를 해결한 정확한 버전을 둔다. 도구가 생성·관리한다.
# pip-tools를 쓰는 경우의 전형적인 패턴
# requirements.in (사람이 작성, 범위)
#   requests>=2.31
pip-compile requirements.in   # → requirements.txt (전이 의존성까지 고정)
pip-sync requirements.txt     # lock 기준으로 환경 정확히 맞춤

업데이트는 lock을 다시 생성(poetry update, pdm update, uv lock --upgrade, pip-compile --upgrade)해서 한다. 이렇게 하면 “평소엔 고정된 버전으로 안정적으로 일하고, 의도적으로 정한 시점에만 일괄 업데이트한다”는 통제가 가능해진다.

라이브러리와 애플리케이션은 다르다

마지막으로 중요한 구분이 있다. 애플리케이션(직접 배포·실행하는 서비스)은 lock 파일을 커밋해 재현성을 확보해야 한다. 반면 라이브러리(다른 프로젝트가 의존하는 패키지)는 의존성을 너무 좁게 고정하면 안 된다. 라이브러리가 requests==2.31.0으로 못 박으면, 그것을 쓰는 애플리케이션이 다른 버전을 못 쓰게 돼 충돌이 생긴다. 라이브러리는 넉넉한 범위(requests>=2.28)로 선언하고, 정확한 고정은 최종 애플리케이션에 맡기는 것이 원칙이다.

이것으로 패키징과 의존성 관리 시리즈를 마무리한다. pip라는 기본기에서 출발해 venv로 격리하고, Poetry·PDM·uv로 통합 관리하며, pyproject.toml로 설정을 표준화하고, wheel·sdist를 빌드해 PyPI에 배포하고, lock으로 재현성까지 확보했다. 도구는 계속 진화하겠지만 “범위로 선언하고 lock으로 고정한다”는 원칙은 변하지 않는다. 이 한 문장만 손에 쥐고 있으면, 어떤 새 도구가 나와도 길을 잃지 않을 것이다.


지난 글: PyPI 배포: 내 패키지를 세상에 공개하기


읽어주셔서 감사합니다. 😊