의존성 지옥(Dependency Hell)을 끝낼 수 있을까? 패키지 매니저를 위한 '통일장 이론'이 등장했다
개발자 생활 15년 차, 솔직히 말해서 우리가 가장 시간을 많이 낭비하는 곳은 코딩이 아니라 설정(Configuration) 과 의존성 관리(Dependency Management) 입니다. ‘내 로컬에서는 되는데?‘라는 말의 9할은 패키지 버전 충돌에서 비롯되죠.
최근 ArXiv에 매우 흥미로운 논문이 하나 올라왔습니다. 제목은 Package Managers à la Carte: a formal model of dependency resolution. 무려 패키지 의존성 해결을 위한 ‘수학적 정형 모델’을 제안하는 논문입니다. 단순히 ‘이 툴이 좋다’ 수준이 아니라, npm, cargo, pip 등이 가진 의존성 해결 로직을 하나의 Package Calculus 로 통합하려는 시도입니다.
이 논문과 이에 대한 Hacker News의 토론이 꽤나 깊이 있는 통찰을 주고 있어, 시니어 엔지니어 관점에서 분석해 보았습니다.
왜 패키지 매니저는 서로 말이 안 통할까?
현재 우리는 ‘패키지 매니저 춘추전국시대’에 살고 있습니다. Node.js는 npm, Python은 pip (혹은 poetry, uv…), Rust는 Cargo를 씁니다. 문제는 이들이 의존성을 해결하는 방식(Semantics)이 미묘하게, 하지만 치명적으로 다르다는 점입니다.
논문의 저자들은 이 파편화(Fragmentation)가 다음과 같은 문제를 야기한다고 지적합니다:
- 다국어 프로젝트의 고통: Python 백엔드와 JS 프론트엔드가 섞인 모노레포에서 의존성을 정확히 표현하기 어렵습니다.
- 암묵적 시스템 의존성: OS 레벨의 라이브러리(
openssl등)는 버전 관리가 안 된 채 방치됩니다. - 보안 사각지대: 전체 의존성 그래프를 한눈에 볼 수 없으니 취약점 파악이 어렵습니다.
저자들은 이를 해결하기 위해 Package Calculus 라는 중간 표현(Intermediate Representation, IR)을 제안합니다. 마치 컴파일러가 소스코드를 LLVM IR로 바꾸듯, 다양한 패키지 매니저의 규칙을 하나의 수식으로 변환하여 생태계 간의 번역을 가능하게 하겠다는 것이죠.
핵심은 ‘다이아몬드 의존성(Diamond Dependency)‘이다
이 논문과 관련해 Hacker News에서 오간 토론 중 가장 인상 깊었던 것은 다이아몬드 의존성 에 대한 지적입니다. 사실상 패키지 매니저의 아키텍처를 결정짓는 단 하나의 질문은 이것입니다.
“서로 다른 버전의 동일한 패키지를 동시에 설치할 수 있는가?”
상황을 가정해 봅시다. App이 Lib A와 Lib B를 씁니다. 그런데 Lib A는 Core v1을, Lib B는 Core v2를 필요로 합니다.
-
지원함 (npm, Cargo, Go 등):
- 두 버전(
Core v1,Core v2)을 모두 설치하고 각각 연결해 줍니다. - 장점: 의존성 지옥에서 비교적 자유롭습니다. 복잡한 트리도 해결 가능합니다.
- 단점: 바이너리 사이즈가 커지고(Bloat), 런타임에 두 버전의 싱글톤 객체가 충돌하는 등 미묘한 버그가 발생할 수 있습니다.
- 두 버전(
-
지원 안 함 (Python, C/C++ Static Linking 등):
- 단 하나의 버전만 선택해야 합니다 (SAT Solver 문제).
- 장점: 런타임이 단순하고 예측 가능합니다.
- 단점: 의존성 지옥 의 주범입니다. 호환되는 버전을 찾지 못하면 설치 자체가 불가능합니다.
HN의 한 유저는 이를 두고 “이것이 패키지 매니저의 핵심 알고리즘적 한계이며, 나머지는 전부 엔지니어링 디테일일 뿐”이라고 일침을 가했습니다. 저도 이 의견에 전적으로 동의합니다. Python 환경에서 pip install 하다가 빨간 에러를 보는 이유의 90%는 바로 이 구조적 한계 때문입니다.
Rust(Cargo)의 영리한 타협
재미있는 점은 Rust의 Cargo가 취하는 방식입니다. Cargo는 다이아몬드 의존성을 지원하지만, SemVer(시맨틱 버저닝) 를 엄격하게 따릅니다.
- 마이너 버전 차이(v1.1 vs v1.2)는 호환된다고 가정하고 하나로 합칩니다(Unification).
- 메이저 버전 차이(v1.0 vs v2.0)는 아예 다른 패키지로 취급하여 둘 다 설치합니다.
이는 타입 안정성을 해치지 않으면서도 의존성 충돌을 최소화하는 매우 실용적인 접근입니다. 하지만 Hacker News 댓글에서도 지적되었듯, 이것도 만능은 아닙니다. SomeType@v1을 기대하는 함수에 SomeType@v2 객체를 넘기면, 런타임이 아닌 컴파일 타임 에 타입 에러가 발생합니다. (물론 런타임에 터지는 것보다는 백배 낫습니다.)
Package Calculus: 현실적인 해결책이 될까?
논문은 이러한 각 생태계의 특성을 모두 수용할 수 있는 정형 모델을 제시합니다. 이론적으로는 훌륭합니다. 만약 이것이 상용화된다면 우리는 다음과 같은 미래를 기대할 수 있습니다.
- Universal Lockfile: 언어 상관없이 프로젝트 전체의 의존성을 하나의 파일로 관리.
- Cross-Ecosystem Security: Python 패키지가 의존하는 C 라이브러리의 취약점까지 한 번에 스캔.
하지만 저는 약간 회의적인 시각도 가지고 있습니다. 패키지 매니저의 동작 방식은 단순히 ‘알고리즘’의 문제가 아니라 런타임(Runtime)의 제약 때문인 경우가 많기 때문입니다.
예를 들어, Node.js는 모듈 시스템 자체가 격리된 스코프를 지원하기 때문에 node_modules 안에 중첩된 의존성을 가질 수 있습니다. 반면 Python은 sys.modules라는 전역 딕셔너리에 모듈을 로드합니다. 패키지 매니저가 아무리 똑똑하게 버전을 분리해도, Python 런타임이 이를 지원하지 않으면 말짱 도루묵입니다. 즉, 이 논문의 아이디어가 실현되려면 패키지 매니저뿐만 아니라 언어 런타임의 변화 도 수반되어야 할 것입니다.
마치며: 그래도 희망은 있다
그럼에도 불구하고 이 연구는 매우 가치가 있습니다. 적어도 우리가 겪고 있는 이 고통이 단순히 ‘도구를 잘못 만들어서’가 아니라, 수학적으로 복잡한 문제(NP-complete)라는 것을 증명하고 체계화했기 때문입니다.
현업 엔지니어들에게 주는 시사점은 명확합니다.
- 의존성은 부채다: 다이아몬드 의존성이 지원된다고 무작정 라이브러리를 추가하면, 결국 런타임 복잡도라는 이자를 치르게 됩니다.
- 생태계를 이해하라: 내가 쓰는 언어가 의존성 충돌을 ‘해결(Merge)‘하는지 ‘회피(Duplicate)‘하는지 알고 있어야 디버깅이 가능합니다.
언젠가 모든 언어의 패키지를 통합 관리하는 ‘One Tool to Rule Them All’이 나올까요? 당장은 어렵겠지만, 적어도 그 초석은 마련된 것 같습니다.
References: