
pip 업데이트를 ‘천천히’ 굴리는 법: dependency cooling과 검증 루프
릴리즈 전날 밤, 그 메시지가 뜬다.
“requirements.txt가 또 흔들렸어요.”
누가 뭘 잘못한 것도 아닌데, 팀 전체가 동시에 긴장한다.
- 지금 올리면 내일 배포가 미끄러질 수 있다.
- 안 올리면 이번 릴리즈에 알려진 버그/취약점을 끌고 간다.
- 올리면, 어디가 깨질지 모른다.
그날도 그랬다. CI가 원래보다 8분 늦게 돌아가고(러너가 밀렸다), 그 사이에 누군가가 “그럼 이참에 패키지 몇 개 더 올리죠”를 꺼냈다. 딱 그 순간부터, 이 PR은 의존성 업데이트가 아니라 ‘프로젝트 토론’이 된다. 토론은 나쁘지 않은데, 릴리즈 전날의 토론은 대개 사고로 끝난다.
의존성 업데이트가 무서운 이유는 ‘한 번의 변경’이 아니라, 변경이 들어오는 속도가 통제되지 않는 상태로 쌓이기 때문이다. 평소에는 미루다가, 급한 날 한 번에 바꾸고, 그 날의 실패 경험이 다음 미루기를 정당화한다.
그래서 이 글의 핵심은 “업데이트를 더 자주 하자”가 아니다. 오히려 업데이트를 늦추는 운영을 이야기한다.
Seth Michael Larson이 소개한 pip v26.0의 상대적 dependency cooling을 출발점으로 삼되(참고자료), 기능 소개가 아니라 팀 운영 규칙으로 번역해보자. 목표는 단순하다. “릴리즈 전날 공포”를 “평일의 반복 작업”으로 내리는 것.
본문에는 링크를 넣지 않고, 읽을거리는 맨 아래 참고자료로만 모았다.
dependency cooling: 업데이트를 막는 게 아니라 ‘속도’를 제한하는 것
dependency cooling을 한 문장으로 요약하면 이거다.
업데이트를 금지하지 말고, 변경의 속도를 감당 가능한 수준으로 늦춘다.
여기서 “늦춘다”는 건 방치하자는 말이 아니다. 리듬을 설계하자는 말이다.
실무에서 의존성 업데이트가 팀을 흔드는 순간은 대부분 비슷하다. 미루다가 한꺼번에 바꾸게 되거나, ‘언제’ 바뀔지 몰라서 늘 불안해지는 순간이다. cooling은 그 불안을 없애려는 장치다. 한 번에 올리는 양을 줄이고(작은 PR), 들어오는 타이밍을 예측 가능하게 만든다(스케줄). 그리고 여기서 가장 중요한 건 “패키지마다 같은 속도”가 아니라 “패키지마다 다른 속도”를 인정하는 태도다.
예를 들어 포매터/린터는 문제가 생겨도 되돌리기 쉽다. 반면 ORM, HTTP 클라이언트, 암호화 라이브러리 같은 건 흔들리면 영향 반경이 넓다. 팀이 불안을 느끼는 건 보통 이런 ‘핵심 라이브러리’ 쪽이다.
그래서 나는 의존성을 한 덩어리로 보지 않는다. 팀 입장에서는 의존성이 아니라 “변경의 위험도”를 운영하는 거다.
- 자주 올려도 되는 묶음(되돌리기 쉬움)
- 천천히 올려야 하는 묶음(영향 반경 큼)
이 분리가 되면, 업데이트 PR이 올라오는 순간의 대화가 달라진다. “큰일 났다”가 아니라 “이번 주에 예정된 변화가 왔다”가 된다.
업데이트 PR을 작게 만드는 게 결국 제일 싸다
cooling을 팀에 붙일 때 제일 먼저 바꾸는 건 도구가 아니라 일정이다.
의존성 업데이트를 사람의 의지로 하면, 바쁜 주에는 안 한다. 그리고 안 한 만큼 다음 주에 한꺼번에 몰아친다. 결국 ‘폭발’을 막을 수 없다.
그래서 업데이트는 “정해진 시간에, 정해진 범위로” 들어오게 만든다. 가장 단순한 시작은 크론이다.
# 월/수/금 오전 10시에만 업데이트 PR 생성(업무 시간)
# 릴리즈 직전 시간대(예: 금요일 오후)는 아예 피한다
0 10 * * 1,3,5 /usr/local/bin/update-deps-and-open-pr
여기서 핵심은 명령어가 아니라, 업데이트가 시스템처럼 굴러가게 만드는 감각이다. 한 번 이렇게 돌아가기 시작하면, 의존성 업데이트가 ‘이벤트’가 아니라 ‘루틴’이 된다.
다만 스케줄만으로는 부족하다. 팀이 합의해야 하는 게이트가 있다.
내가 팀에 제일 먼저 박아두는 건 거창한 규정이 아니라, 릴리즈를 지키는 한 장면이다. 예전에 릴리즈 브랜치에 “사소한 업데이트”를 얹었다가, 다음 날 아침에만 테스트가 깨져서 반나절을 날린 적이 있다. 그 이후로는 단순하게 간다. 릴리즈 브랜치에서는 업데이트를 안 한다(보안만 예외). 업데이트 PR은 자동 머지를 막고, 한 번은 사람이 눈으로 보고 “지금 올려도 되는 변경”인지 확인한다. 보안 패치처럼 미룰수록 위험이 커지는 건, 그때만 속도를 올린다.
이 규칙들은 장황한 문서보다 PR 템플릿 한 줄이 더 오래 산다. 예를 들어:
- “이 PR은 정해진 주기상 올라온 업데이트이며, 릴리즈 브랜치에는 적용하지 않는다.”
이 한 줄이 ‘릴리즈 전날 requirements 흔들림’을 예방하는 첫 장치가 된다.
그리고 한 가지 더. 업데이트 PR을 작게 만들려면 “다 올리지 않기”가 아니라 “나눠 올리기”가 필요하다.
- 핵심 라이브러리 업데이트는 따로(리뷰와 검증을 집중)
- 도구성 라이브러리 업데이트는 묶어서(비용 최소화)
cooling은 결국 ‘속도 제한’이자 ‘변경량 쪼개기’다.
여기서 “상대적”이라는 감각이 하나 더 들어간다. 어떤 패키지는 어제도 올렸고, 어떤 패키지는 반년 만에 올린다. 둘을 같은 규칙으로 다루면 늘 한쪽이 터진다. 그래서 마지막 변경 시점(또는 팀이 체감하는 위험도)에 따라, 이번 PR에서 허용하는 업데이트 범위를 다르게 잡는다. 결국 팀이 원하는 건 “최신”이 아니라 “통제된 변화”다.
가벼운 검증 루프: 빠른 확인을 먼저 붙여서 불안을 줄인다
업데이트 PR이 정해진 리듬으로 올라오기 시작하면, 다음 병목은 검증이다.
모든 프로젝트가 튼튼한 자동 테스트를 갖고 있지 않다. 갖고 있어도 시간이 오래 걸릴 수 있다. 그 상태에서 업데이트를 자주 올리면, 팀은 다시 이벤트 모드로 돌아간다. “테스트 기다리느라 하루 날림”이 반복되면 결국 업데이트를 끊는다.
그래서 나는 검증을 두 겹으로 둔다.
- 가벼운 검증(빠르게 실패를 걸러냄)
- 무거운 검증(정식 테스트)
여기서 uv는 ‘노트북 도입’ 얘기가 아니라, 가벼운 검증을 싸게 돌릴 수 있는 실행 경로로만 쓴다. 팀이 합의한 한 줄로 실행을 고정하면, “누구 로컬에선 되고 누구 로컬에선 안 됨” 같은 마찰이 줄어든다.
# pip/uv 실행 경로를 팀 표준으로 고정(예시는 상황에 맞게 조정)
pip install -U pip
pip install -U -r requirements.txt # constraints/lock이 있으면 그 규칙을 우선한다
# 일회성 검증(환경을 어지럽히지 않게)
uvx python -c "import yourpkg; print('ok')"
이 정도만 붙여도 업데이트 PR은 “머리 아픈 변화”에서 “확인 가능한 변화”로 내려온다.
검증 코드는 짧게: 제너레이터 표현식으로 실패만 모은다
검증 루프가 커지는 순간부터, 검증 코드 자체가 또 하나의 제품이 된다. 그러면 아무도 만지기 싫어지고, 업데이트 PR은 다시 ‘특별한 이벤트’가 된다.
그래서 검증 코드는 가능한 짧게 만든다. Python Morsels가 커스텀 comprehension/제너레이터 표현식을 다루는 글을 보면서(참고자료), 나는 검증도 이 방식이 유지비가 제일 낮다고 느꼈다.
- 체크 목록은 나열한다
- 실행은 한 번에 소비한다
- 실패만 모아서 출력한다
checks = (
(name, fn())
for name, fn in [
("import_yourpkg", lambda: __import__("yourpkg") is not None),
("cli_help", lambda: __import__("subprocess").run(["python", "-m", "yourpkg", "--help"], check=False).returncode == 0),
]
)
failed = [name for name, ok in checks if not ok]
if failed:
raise SystemExit(f"dependency update check failed: {failed}")
이 스크립트가 훌륭해서가 아니라, 팀이 계속 유지할 수 있어서 좋다.
- 체크를 추가하려면 한 줄만 늘린다
- 실패는 이름으로 남는다
- PR 본문에 결과를 붙이기 쉽다
여기까지 해두면, 릴리즈 전날 requirements가 흔들릴 때 팀의 반응이 바뀐다.
“그럼 업데이트 해볼까?”가 아니라,
“이번 주 업데이트 PR에서 이미 걸러졌어. 큰 변화는 다음 슬롯으로 미루자.”
cooling은 ‘느림’이 아니라 예측 가능성이고, 작은 검증 루프는 그 예측 가능성을 실제로 굴러가게 만드는 장치다.
참고자료
- Seth Michael Larson — Relative “Dependency Cooling” in pip v26.0 with crontab
- https://sethmlarson.dev/pip-relative-dependency-cooling-with-crontab
- Python Morsels — Invent your own comprehensions in Python
- https://www.pythonmorsels.com/custom-comprehensions/
- Rodrigo Girão Serrão (mathspp.com) — TIL #140 – Install Jupyter with uv
- https://mathspp.com/blog/til/install-jupyter-with-uv
댓글
댓글 쓰기