파이썬 uv.lock 충돌 해결법: uv sync --locked와 --frozen 차이와 CI 적용 기준

파이썬 uv.lock 충돌 해결법의 핵심은 uv sync --locked와 --frozen의 검증 차이를 구분하는 것이다. CI에서 잠금 파일 갱신이 필요하다는 오류가 나오면, 먼저 의존성 선언과 잠금 파일이 어긋났는지 확인해야 한다. --frozen으로 바꿔 설치가 진행되더라도 그 불일치가 해결된 것은 아니다.

uv.lock과 실행 환경의 관계부터 확인하기

pyproject.toml은 프로젝트가 요구하는 의존성을 선언하고, uv.lock은 그 조건을 해결한 패키지 버전 정보를 기록한다. uv sync는 잠금 파일에서 설치 대상을 선택해 프로젝트 환경에 반영한다. 따라서 선언, 잠금, 설치 환경은 서로 연결되어 있지만 같은 대상은 아니다.

예를 들어 의존성을 직접 추가한 뒤 pyproject.toml만 커밋하면, 저장소의 선언에는 새 패키지가 있지만 잠금 파일에는 해당 변경이 빠질 수 있다. 이 상태에서 --locked는 잠금 파일을 고치는 대신 오류를 낸다. 반면 --frozen은 최신 선언과 일치하는지 검사하지 않고 기존 잠금 내용을 사용한다.

📌 핵심 요약

  • --locked: 잠금 파일의 정합성을 검사하고, 갱신이 필요하면 실패한다.
  • --frozen: 정합성 검사를 생략하고 기존 잠금 파일을 사용한다.
  • 두 옵션 모두 잠금 파일이 없으면 실패하며, sync 실행 시 설치 환경은 변경될 수 있다.

여기서 ‘최신’은 저장소에 공개된 가장 높은 패키지 버전을 뜻하지 않는다. 현재 프로젝트 선언과 잠금 파일이 맞는다는 뜻이다. 새 버전이 배포됐다는 이유만으로 기존 잠금 파일이 오래된 것으로 판정되지는 않는다.

근거: [Astral 공식 문서의 잠금·동기화 설명](https://docs.astral.sh/uv/concepts/projects/sync/).

파이썬 uv.lock 충돌 해결법: uv sync --locked와 --frozen 차이와 CI 적용 기준

uv sync --locked와 --frozen 차이: 상황별 명령 판단표

CI에서 선언과 잠금의 불일치를 차단하려면 --locked를 기본값으로 삼는 편이 적합하다. --frozen은 기존 잠금 파일을 그대로 소비해야 하는 단계에 어울린다. 속도나 오류 회피보다, 해당 단계가 무엇을 검증해야 하는지로 선택하는 것이 정확하다.

상황 선택할 명령 검증·변경 범위
CI에서 선언 누락 차단 uv sync --locked 정합성 검사 후 설치. 잠금 갱신이 필요하면 실패
기존 잠금 기준으로 설치 uv sync --frozen 최신 선언과의 일치 검사 생략. 잠금 파일은 필요
설치 없이 정합성 확인 uv lock --check 잠금 최신 여부 검사. 환경 동기화 없음
잠금 파일 존재만 확인 uv lock --check-exists 최신 여부 검사 생략. 설치하지 않음
의존성 선언 변경 반영 uv lock 필요한 잠금 변경 생성. 설치는 별도
특정 패키지 업그레이드 uv lock --upgrade-package httpx 선언 범위 안에서 대상 패키지 갱신 요청

uv lock --check는 잠금 검증 관점에서 --locked와 동등하지만 패키지를 설치하지 않는다. 마찬가지로 --check-exists의 검증 방식은 --frozen에 대응하지만, 환경을 동기화하는 명령은 아니다. 검증 단계와 설치 단계를 나누면 실패 원인을 파악하기 쉬워진다.

명령별 동작 근거: [Astral CLI 공식 레퍼런스](https://docs.astral.sh/uv/reference/cli/).

pyproject.toml 변경 후 --locked 오류 해결 순서

대표적인 오류 문구는 다음과 같다. 이 문장은 잠금 파일을 갱신해야 하지만 현재 실행에서는 갱신을 허용하지 않았다는 뜻이다.

The lockfile at uv.lock needs to be updated, but --locked was provided.
  1. 변경 의도를 확인한다. git diff -- pyproject.toml uv.lock으로 의존성 추가, 버전 제약 변경, 잠금 파일 누락을 살핀다. 이미 커밋한 변경이라면 PR의 기준 브랜치와 비교한다.
  2. 승인할 선언을 확정한다. 실수로 바뀐 조건은 되돌리고, 필요한 의존성 변경은 유지한다.
  3. 잠금 파일을 갱신한다. 로컬에서 uv lock을 실행하고 변경된 패키지와 버전을 검토한다.
  4. 잠금 기준으로 설치하고 테스트한다. uv sync --locked 후 프로젝트 테스트를 실행한다.
  5. 관련 파일을 함께 커밋한다. 선언 변경에 잠금 갱신이 필요했다면 pyproject.toml과 uv.lock을 함께 반영한다.
uv lock
git diff -- pyproject.toml uv.lock
uv sync --locked
uv run --locked pytest

위 테스트 명령은 프로젝트에 pytest가 포함된 경우의 예시다. uv lock은 모든 패키지를 무조건 최신 버전으로 올리는 명령이 아니다. 기존 잠금 버전을 우선 활용하며, 전체 업그레이드가 목적이라면 uv lock --upgrade를 별도 변경으로 실행하는 편이 검토하기 쉽다.

또한 pyproject.toml이 바뀌었다고 항상 잠금 갱신이 필요한 것은 아니다. 버전 제약을 바꿔도 기존 잠금 버전이 여전히 허용되면 최신 상태로 인정될 수 있다. 반대로 새 의존성을 추가하거나 기존 잠금 버전을 배제하는 조건을 넣으면 갱신이 필요하다.

오류 문구가 기록된 GitHub 이슈 #10273은 특히 업그레이드 요청과 잠금 유지 옵션의 충돌을 다룬 사례다. 이를 모든 선언 불일치의 재현 사례로 해석해서는 안 된다. 2026년 6월 1일 후속 댓글에는 uv 0.9.6에서 --locked와 --upgrade의 동시 사용이 인자 충돌로 표시된 기록도 있어, 버전에 따라 오류 표현이 달라질 수 있다.

오류 기록과 버전별 차이: [Astral uv 저장소 이슈 #10273](https://github.com/astral-sh/uv/issues/10273).

Git 병합으로 생긴 uv.lock 충돌 해결법

Git 충돌 마커가 들어간 파일과, 문법은 정상인데 선언과 맞지 않는 잠금 파일은 구분해야 한다. 전자는 먼저 유효한 잠금 파일로 복구해야 한다. 충돌 마커가 남은 상태에서 --frozen을 지정해도 손상된 파일을 정상적으로 사용할 수 있게 되는 것은 아니다.

Astral uv 저장소 관리자는 2025년 10월 17일 이슈 #16348에서 한쪽 브랜치의 잠금 파일을 복원한 뒤 다시 잠그는 접근을 지지했다. 잠금 파일을 삭제하면 기존 버전을 유지할 기준까지 사라지므로 일반적인 해결책으로 피하라는 설명이다. 이 방식은 불필요하게 넓은 의존성 변경을 줄이는 데 의미가 있다.

일반적인 merge 작업에서 실행할 예시

먼저 pyproject.toml의 충돌을 해결해 최종 의존성 선언을 확정한다. 아래 예시는 일반적인 merge 중 현재 브랜치의 uv.lock을 출발점으로 선택하는 경우다. 양쪽 잠금 파일의 변경 의도를 확인한 뒤 선택해야 한다.

git checkout --ours -- uv.lock
uv lock
git diff -- uv.lock
uv sync --locked
uv run --locked pytest
git add pyproject.toml uv.lock

상대 브랜치를 기준으로 삼으려면 --theirs를 선택한다. 다만 rebase에서는 ours와 theirs가 가리키는 대상이 직관과 다를 수 있으므로, 위 명령을 그대로 적용하지 말고 작업 상태와 파일 내용을 확인해야 한다.

한쪽 브랜치에 의도적인 보안 업데이트가 있었다면 최종 잠금 결과에 그 변경이 유지됐는지도 확인한다. 충돌 마커가 사라졌다는 사실만으로 양쪽 브랜치의 변경 목적이 모두 반영됐다고 볼 수는 없다.

병합 복구 접근의 근거: [Astral uv 저장소 이슈 #16348](https://github.com/astral-sh/uv/issues/16348).

GitHub Actions CI 적용 기준과 uv 버전 고정

2026년 10월 기준 확인한 Astral의 GitHub Actions 공식 가이드는 설치 예제로 uv sync --locked --all-extras --dev를 제시한다. 커밋된 잠금 파일을 검증하면서 선택한 의존성을 설치하는 구성이다. CI에서 자동으로 잠금을 고쳐 통과시키기보다, 변경이 필요한 상태를 개발 단계로 돌려보내는 운영에 맞는다.

# uv와 Python 설정 이후의 CI 실행 단계
- name: Install dependencies
  run: uv sync --locked --all-extras --dev

- name: Run tests
  run: uv run --locked pytest

--all-extras는 모든 선택 의존성을 포함하지만 모든 의존성 그룹을 뜻하지는 않는다. 별도 테스트 그룹이 있다면 해당 그룹을 명시해야 한다. 서로 배타적인 extras가 있는 프로젝트라면 전체 설치 대신 작업별로 필요한 조합을 선택한다.

Astral은 CI에서 uv 버전 고정도 권장한다. 팀이 검증한 버전을 astral-sh/setup-uv의 version 입력에 지정하고 로컬과 CI에서 맞추는 것이 좋다. 잠금 파일을 소비하는 도구 버전까지 통제하면 환경 차이를 추적하기 수월해진다.

CI 설치 예시와 버전 고정 권고: [Astral GitHub Actions 공식 가이드](https://docs.astral.sh/uv/guides/integration/github/).

잠금 파일 호환성도 확인할 대상이다. 공식 문서는 같은 마이너 릴리스 안의 패치 버전 간에는 완전한 호환성을 보장하지만, 마이너 릴리스 사이에서는 잠금 파일을 거부할 수 있다고 설명한다. uv 업데이트를 의존성 변경과 함께 무심코 섞기보다, 도구 버전 변경의 영향을 따로 검토하는 편이 원인 분석에 유리하다.

운영 관점에서 중요한 기준은 CI 통과가 어떤 사실을 증명하는가다. --locked는 현재 선언과 잠금의 일치 여부까지 검사한다. --frozen으로 대체하면 그 검증 범위가 줄어들므로, 앞선 단계에서 같은 커밋의 정합성을 확인했는지 설명할 수 있어야 한다.

잠금 파일 호환성 근거: [Astral 의존성 해결 공식 문서](https://docs.astral.sh/uv/concepts/resolution/).

extras 충돌과 환경 동기화에서 놓치기 쉬운 점

오류가 실제 패키지 제약 충돌이라면 잠금 옵션만 바꿔서는 해결되지 않는다. 서로 다른 extras나 의존성 그룹이 양립할 수 없는 조건을 요구한다면, 함께 사용해야 하는 구성인지부터 판단해야 한다. 각각 별도로 사용하는 구성이면 [tool.uv]의 conflicts로 상호 배타 관계를 선언할 수 있다.

단, 충돌한다고 선언한 조합을 같은 환경에 동시에 설치할 수 있게 되는 것은 아니다. 예를 들어 배타적인 extras를 선언한 뒤 --all-extras로 모두 선택하면 오류가 발생한다. 이때는 CI 작업을 나누고 각 작업에 필요한 extra나 그룹만 설치하는 방식이 적합하다.

배타적인 의존성 조합의 처리 근거: [Astral 의존성 해결 문서의 conflicts 설명](https://docs.astral.sh/uv/concepts/resolution/).

💡 참고: --locked와 --frozen은 설치 환경을 읽기 전용으로 만드는 옵션이 아니다. uv sync는 기본적으로 정확한 동기화를 수행하므로 설치 대상으로 선택되지 않은 패키지를 제거할 수 있다. 설치 변경 없이 확인만 하려면 uv lock --check를 사용한다.

실무에서는 잠금 갱신, 검증, 설치를 구분할수록 변경을 검토하기 쉽다. 의존성을 바꿀 때는 uv lock으로 결과를 만들고 테스트하며, CI에서는 uv sync --locked로 그 결과가 선언과 맞는지 확인한다. --frozen은 기존 잠금 파일을 그대로 사용할 이유와 사전 검증 경로가 명확할 때 선택하면 된다.