한 줄 정의
의존성 관리(dependency management)란 우리가 통제하지 못하는 라이브러리·패키지들의 거대한 네트워크를, 그것이 시간에 따라 제각각 변해가는 와중에 관리하는 일입니다. 핵심 난관은 단 하나로 수렴합니다 — ‘서로 소통하지 않는 조직들이 각자의 일정으로 업그레이드하면서도 요구사항 충돌을 어떻게 피할 것인가?’ 오늘날 업계 표준인 SemVer는 변경의 위험도를 사람이 번호 하나로 추정해 압축한 것인데, 이 장의 핵심 주장은 하이럼의 법칙 때문에 ‘내 API만 보고는 호환성을 증명할 수 없으며’, 추정(SemVer)보다 증거(테스트·CI)가 더 거대한 의존성 네트워크를 충실하게 관리한다는 것입니다. 그리고 그 전에 더 근본적인 조언이 있습니다 — 되도록 의존성 관리(통제 불가)를 버전 관리(통제 가능)로 끌어내려라.
쉽게 말하면
내가 만드는 제품은 사실 내가 손수 만든 부분보다 남이 만든 부품이 더 많습니다. 그리고 그 부품들조차 또 다른 회사의 더 작은 부품으로 조립돼 있죠(전이 의존성). 나는 이 공급사들을 통제하지 못합니다. 그들은 나와 상의하지 않고 자기 일정대로 ‘개선된 신버전’을 찍어냅니다.
공급사는 새 버전을 보낼 때마다 상자에 라벨을 붙여줍니다 — “완전 호환(초록)”, “기능만 추가(노랑)”, “호환 안 됨(빨강)“. 이게 SemVer입니다. 편리해 보이지만 함정이 있습니다. 그 라벨은 공급사가 자기 도면만 보고 찍은 ‘추측’ 이라는 점입니다. 내 제품에서 그 부품이 실제로 어떻게 끼워져 있는지, 혹시 그 부품의 모서리가 옆 부품의 무게까지 떠받치고 있는지를 공급사는 전혀 모릅니다. 그래서 “완전 호환” 라벨이 붙은 부품이 내 제품을 무너뜨릴 수도 있고(“호환된다며?”), 반대로 “호환 안 됨” 딱지가 붙었는데 막상 끼워보니 멀쩡할 수도 있습니다.
그렇다면 진짜로 안전한지 아는 유일한 방법은 무엇일까요? 라벨을 읽는 게 아니라, 그 부품을 실제로 끼워서 제품을 돌려보는 것입니다. 테스트와 CI가 바로 이 “끼워서 돌려보기”입니다.
이 장 전체는 결국 “공급사가 붙인 라벨(추정)을 믿지 말고, 조립해서 돌려본 결과(증거)를 믿어라. 그리고 가능하면 부품을 아예 우리 공장 안으로 들여와 직접 통제해라” 는 이야기입니다.
왜 중요한가?
한두 계층 위에 바로 소프트웨어를 올리던 시절은 지났습니다. 오늘날 소프트웨어는 수많은 의존성 기둥 위에 지어지고, 오픈 소스 모델이 성장하면서 유명 프로젝트들의 의존성 그래프도 계속 커지고 있습니다. 그래서 의존성 관리는 소프트웨어 엔지니어링 정책 중 가장 중요하면서도 이해도가 가장 낮은 문제가 되었습니다.
문제의 무게는 ‘의존성 하나를 어떻게 임포트하나?‘에 있지 않습니다. 그건 너무 좁은 정의입니다. 진짜 문제는 수많은 의존성으로 구성된 네트워크와, 그 네트워크에 미래에 일어날 변화까지 관리하는 것입니다. 내가 직접 의존하는 노드 몇 개 뒤로, 딸려 들어오는 전이 의존성이 줄줄이 매달려 있고, 시간이 흐르면 그중 어딘가가 새 버전을 내고 그중 몇은 중대한 업데이트입니다. 이 업그레이드 도미노를 어떻게 감당하느냐가 핵심입니다.
이게 없다면(혹은 잘못하면) 어떻게 될까요? 같은 라이브러리의 호환되지 않는 두 버전에 동시에 묶이는 다이아몬드 의존성에 빠지거나, 만족스러운 버전 조합이 아예 존재하지 않는 의존성 지옥(dependency hell) 에 갇힙니다. 단 하나의 의존성을 보안 패치하려다 네트워크 전체를 업그레이드해야 하는 사태로 번지기도 합니다.
하이럼의 법칙 — 이 장을 관통하는 전제
“사용자 수가 충분히 많아지면, 시스템이 약속하지 않은 것까지 포함해 관찰 가능한 모든 동작에 누군가는 의존하게 됩니다.” 자세한 배경은 Ch01 소프트웨어 엔지니어링이란에 있습니다. 이 장에서는 이 법칙이 ‘왜 SemVer 라벨이 거짓말이 되는가’를 설명하는 핵심 도구로 쓰입니다.
핵심 내용
의존성 관리 vs 버전 관리 — 통제권의 문제
의존성 관리와 가장 가까운 주제는 소스 버전 관리입니다. 둘 다 소스 코드를 시간 축 위에서 어떻게 다루느냐를 이야기하지만, 결정적 차이가 하나 있습니다 — 이 하위 프로젝트의 개발·업데이트·관리를 우리 조직이 통제하는가?
| 관점 | 버전 관리(통제 가능) | 의존성 관리(통제 불가) |
|---|---|---|
| 코드 가시성 | 코드베이스가 공유되어 어떻게 쓰이는지 보임 | 속을 들여다볼 수 없음, 조직 외부에서 변경 |
| 변경 안전성 | 직접 빌드·테스트하며 기존 코드 손상 방지 | 업스트림이 내 빌드·테스트를 깨뜨릴 수 있음 |
| 손발 맞추기 | 가능 | 업스트림은 내 코드와 발 맞춰주지 않음 |
여기서 이 장 전체를 떠받치는 실천적 조언이 나옵니다.
의존성 관리의 첫 번째 원칙
다른 조건이 모두 같다면, 의존성 관리 문제보다는 소스(버전) 관리 문제를 택하세요. 생각하기도 훨씬 쉽고 처리 비용도 훨씬 저렴합니다.
구글이 단일 모노리포에서 모든 걸 관리하며 소스 관리 정책을 거대한 규모까지 일관되게 적용하는 이유가 이것입니다. ‘조직’을 팀이 아니라 회사 전체로 넓게 정의하면, 많은 의존성 문제가 사실은 통제 가능한 버전 관리 문제로 바뀝니다.
다이아몬드 의존성 — 요구사항 충돌의 대표 사례
대부분의 난제는 한 문장으로 요약됩니다 — ‘의존성 네트워크상의 두 의존성 사이에서 요구사항이 충돌할 때 어떻게 해결하는가?’ 운영체제·언어·컴파일러 같은 플랫폼 버전이 원인일 수도, 업그레이드한 라이브러리가 기존 버전과 호환되지 않아서일 수도 있습니다. 그 대표적 형태가 다이아몬드 의존성 문제(diamond dependency problem) 입니다.
성립하려면 의존성 계층이 최소 세 개 필요합니다. 여기서 libbase는 네트워크에서 둘 이상의 노드가 함께 쓰는 어떤 소프트웨어든 될 수 있습니다.
flowchart TD libuser --> liba libuser --> libb liba -->|libbase v2 필요| libbase libb -->|libbase v1 필요| libbase classDef conflict fill:#ffe3e3,stroke:#e5484d; class libbase conflict;
libbase가 하위 호환되지 않는 업데이트를 했는데, liba와 libb가 동시에 따라오지 못하는 상황(주로 서로 다른 조직이라)이 문제입니다. liba는 신버전 libbase를, libb는 구버전을 요구하면, 둘을 함께 쓰는 libuser에 두 버전을 동시에 넣을 보편적 방법이 없습니다. 현실의 네트워크에서는 이런 잠재적 다이아몬드가 수십 배 더 복잡하게 얽혀 있습니다.
충격은 언어마다 다르다
| 언어 | 여러 버전 동시 내장 | 결과 |
|---|---|---|
| 자바 | 가능 (셰이딩/버저닝으로 심볼 이름 변경) | 함수 수준에선 동작 |
| C++ | 사실상 불가 | ODR(One Definition Rule) 위반 → 예측 불가 버그·정의되지 않은 동작(UB) |
다만 자바처럼 여러 버전을 함께 넣는 방식도 만능은 아닙니다. 함수 수준에서는 이름을 바꿔 동작하게 할 수 있지만, 의존성 사이에 주고받는 타입(type) 에는 통하지 않습니다. libbase v1이 정의한 맵을 의미 그대로 간직한 채 libbase v2의 API로 넘길 방법이 없기 때문입니다. 심볼 숨기기·이름 바꿔 중복 적재는 충격을 다소 완화할 뿐 보편적 해결책이 못 됩니다.
요구사항 충돌을 쉽게 푸는 유일한 길은 모두와 호환되는 더 상위(혹은 하위) 버전을 찾는 것 뿐입니다. 그게 불가능하면 로컬에서 직접 패치해야 하는데, 비호환의 근본 원인을 처음 발견한 엔지니어가 직접 파헤쳐야 하므로 매우 어렵습니다(그가 libbase와 liba를 둘 다 고칠 만큼 깊이 이해할 확률은 낮습니다). 이 범용 해법을 찾아낸다면 의존성 관리 분야에 기념비적 기여가 될 정도로 어려운 문제입니다.
의존성 임포트하기 — 공짜처럼 보이지만 계약이다
처음부터 새로 짜는 것보다 검증된 인프라를 재사용하는 게 분명히 낫습니다. 모든 입문자가 JSON 파서와 정규식 엔진을 다시 구현해야 한다면 기술은 한 발짝도 못 나아갑니다. 문제는 언제 그 재사용이 진짜 저렴한가 입니다.
'프로그래밍'에서는 거의 공짜, '소프트웨어 엔지니어링'에서는 미묘하게 비싸진다
오늘만 쓸 프로그램이라면 눈에 띄는 의존성을 다 가져다 써도 됩니다 — 책임질 일도, 업그레이드를 계획할 일도 없습니다. 하지만 시간(유지보수) 이 끼어드는 순간 똑같은 의존성이 비싸집니다. 업그레이드 계획이 없던 의존성도 보안 취약점이 발견될 수 있고, 외부 요인으로 플랫폼을 갈아타야 할 수도, 네트워크가 팽창하며 의도와 무관하게 업그레이드를 강요당할 수도 있습니다. ‘동작하게 만들었다(got it to work)‘와 ‘지원되는 방식으로 동작한다(working in a supported fashion)‘는 전혀 다른 상태입니다.
호환성 약속의 스펙트럼
의존성 제공자가 미래에 대해 무엇을 약속하느냐는 프로젝트마다 천차만별입니다. 유지보수 비용을 가늠하려면 ‘호환성이 얼마나 지켜지나, 진화가 얼마나 빠른가, 변경을 어떻게 처리하나, 각 버전의 지원 기간은?‘을 물어야 합니다.
| 프로젝트 | 호환성 약속 | 특징 |
|---|---|---|
| C++ 표준 라이브러리 | 거의 무한대 하위 호환 (API + ABI) | 구버전용 빌드 바이너리가 신버전에서도 링크. SD-8 문서로 변경 범위 정의 |
| 자바 | 소스·바이너리 모두 하위 호환 | 옛 JAR이 새 버전과 어울려 구동 |
| Go | 소스만 호환, 바이너리는 X | 빌드 버전이 다른 바이너리끼리 링크 불가 |
| Abseil(구글) | 제한된 API 호환 + 자동 마이그레이션 도구 | 영원한 호환성 거부, ABI 변경 권한 보유. 비용을 친화적 방식으로 전가 |
| Boost | 호환성 보장 안 함 | ’실험적 증명의 장’이라는 목적상 우선순위가 다를 뿐, 기술력 부족이 아님 |
여기서 중요한 통찰은 호환성은 기술력 문제가 아니라 프로젝트의 목적·우선순위 문제 라는 점입니다. Boost 개발자들은 표준 라이브러리 개발자만큼 전문가지만, ‘실험’이라는 목적상 호환성 보장에 우선순위를 두지 않을 뿐입니다. Abseil이 택한 길이 특히 흥미롭습니다 — 호환성을 위해 API를 박제하는 대신, 파괴적 변경을 하되 소비자를 위한 자동 리팩터링 도구를 함께 제공하기로 약속합니다. 예상치 못한 변경 비용을, 사용자가 직접 떠안는 대신 제공자가 도구로 흡수하는 정책입니다.
임포트 전에 던져야 할 질문들
구글은 의존성을 들이려는 엔지니어에게 두 종류의 질문을 권합니다. 의존성 자체에 대해서는 — 실행 가능한 테스트가 있는가, 모두 통과하는가, 제공자는 누구이며 호환성 정책은 무엇인가, 얼마나 인기 있고 파괴적 변경은 얼마나 잦은가. 우리 조직 관점에서는 — 같은 기능을 직접 구현하면 얼마나 복잡한가, 최신으로 유지하면 어떤 이점이 있는가, 업그레이드는 누가 할 것이며 난이도는 어느 정도일까. 임포트와 재구현 중 무엇이 장기적으로 저렴할지 알려줄 완벽한 공식은 없습니다(솔직히 틀릴 때가 더 많습니다).
third_party의 함정 — 앨리스 이야기
구글조차 “더 잘할 수 있었다”고 인정하는 영역입니다. 외부에서 가져온 의존성은 모노리포의 third_party 디렉터리에 들어가고, 절차는 단순합니다 — ① 구글 빌드 시스템에서 빌드되는지 확인, ② 같은 패키지가 없는지 확인, ③ 유지보수할 OWNERS 두 명 이상 등록. 문제는 이 가벼운 절차가 만드는 장기 부채입니다.
flowchart TD A["앨리스가 시연용으로<br/>오픈소스 패키지를 third_party에 추가"] --> B["옆 팀 찰리가 검색으로 찾아<br/>의존성으로 추가 (앨리스는 모름)"] B --> C["전이 의존성으로 간접 사용 확산<br/>수천 개 프로젝트가 의존"] C --> D["앨리스·밥은 다른 팀으로 이동<br/>OWNERS는 방치"] D --> E["시간이 지날수록 하이럼의 법칙이<br/>그 특정 버전을 강하게 옥죔"] E --> F["보안 취약점 발견 → '반드시 업그레이드'<br/>그러나 경험자도, 이해한 사람도 없음"]
핵심은 방치된 시간이 곧 비용 이라는 것입니다. 패키지가 코드베이스에 오래 머물수록 직간접 의존성이 늘고, 변하지 않는 그 버전에 하이럼의 법칙이 점점 더 강하게 들러붙어 나중의 마이그레이션을 어렵게 만듭니다. 그래서 인기는 높지만 호환성을 약속하지 않는 프로젝트(예: Boost)에, 수명이 수십 년 단위인 코드베이스가 의존하는 것은 특히 위험합니다.
(이론상의) 네 가지 의존성 관리 모델
이상적인 해법이라면 모든 요구사항 충돌로부터 보호하고, 동적인 생태계에서도 통하며, 시간을 간과하지 않고, 조직 간 조율·가시성 제한 없이 합리적 컴퓨팅 자원만으로 작동해야 합니다. 이 요건을 일부라도 충족하는 모델 네 가지가 있습니다.
① 변경 불가 — 정적 의존성 모델
변경 자체를 금지합니다. 사용자 동작에 영향 없는 버그 수정만 허용하죠. 가장 간단하고, 사실 모든 조직이 처음엔 이렇게 시작합니다. 신생 프로젝트가 수십 년 살아남으며 의존성을 물 흐르듯 갈아야 할 줄 알고 시작하는 경우는 드물기 때문입니다. 한계는 명확합니다 — 프로젝트가 오래 살아남을수록 ‘안정적’이라는 가정이 틀릴 확률이 커지는데, 보안 버그가 언제 터질지 알려줄 조기 경보가 없습니다.
② 유의적 버전(SemVer) — 사람이 위험을 번호로 추정
오늘날 가장 대표적인 방법입니다. 2.4.27처럼 메이저·마이너·패치 세 숫자로 표기합니다.
| 자리 | 의미 |
|---|---|
| 메이저 | API 변경 — 기존 코드를 깨뜨릴 수 있음 |
| 마이너 | 순수 기능 추가 — 기존 코드는 안 깨뜨림 |
| 패치 | API에 영향 없는 내부 개선·버그 수정 |
이 덕분에 libbase ≥ 1.5 같은 요구사항 표현이 가능합니다(1.5·1.5.1·1.6은 되지만 1.4.9나 2.x는 안 됨). 의존성 네트워크는 노드(컴포넌트)와 에지(버전 요구사항)의 집합이 되고, 모든 요구사항을 동시에 만족하는 버전 집합을 찾는 일은 충족 가능성 솔버(SAT solver) 가 맡습니다. 이 과정을 버전 선택(version selection), 만족스러운 조합이 없는 상태를 의존성 지옥 이라 부릅니다. 단, SAT 솔버가 해가 존재함을 보장해주지는 않습니다.
③ 하나로 묶어 배포하기
구동에 필요한 의존성을 모두 찾아 애플리케이션과 함께 배포합니다(리눅스 배포판이 대표적). 여기서 배포자(distributor) 라는 새 역할이 등장해, 상호 호환되는 버전 조합을 찾고 패치하고 검증합니다. 외부 사용자는 ‘72개 라이브러리에 의존’ 대신 ‘레드햇 버전 N에 의존’이라고 말하면 되니 편리합니다 — 네트워크 전체를 하나의 초거대 의존성으로 대체해 단일 버전 번호를 붙인 셈입니다. 버전 선택의 부담을 전문 배포자에게 위임하는 모델입니다.
④ 헤드에서 지내기(Live at Head)
트렁크 기반 개발을 업스트림 의존성에까지 확장한, 구글 일부가 추진하는 모델입니다. SemVer를 버리고, 의존성 제공자가 변경을 커밋하기 전에 생태계 전체를 대상으로 검증합니다. 모든 컴포넌트가 항상 최신에 의존하고, 소비자가 수용하기 어려운 변경은 허용하지 않습니다. API/동작이 달라지는 변경은 다운스트림의 CI에서 포착되어 커밋이 막히고, 불가피하다면 제공자가 다운스트림을 먼저 업데이트하거나 자동 마이그레이션 도구를 제공할 때만 진행합니다.
핵심은 책임 주체의 이동 입니다. 더 이상 SemVer에게 ‘이 버전은 안전할까?‘라고 묻지 않고, 테스트와 CI로 ‘실제로’ 안전한지를 실험합니다. 변경이 다운스트림에 미치는 영향을 검증할 부담이 API 제공자에게 넘어가므로, 오픈 소스 생태계가 이 길로 첫발을 떼게 만들 동기를 주기는 쉽지 않습니다. 버전 선택은 ‘모든 것의 가장 최근 안정 버전이 뭐죠?‘로 단순해집니다.
SemVer의 한계 — 라벨은 거짓말을 한다
SemVer는 작은 규모에서는 훌륭히 작동하지만, 규모가 커지면 네 가지 문제가 드러납니다.
지나치게 구속할 수 있다(거짓 음성)
라이브러리의 인터페이스들이 서로 독립적인 경우가 대부분인데, SemVer는 라이브러리 단위로 버전을 매깁니다. libbase가 Foo와 Bar 두 함수만 제공하고 liba·libb는 Foo만 쓴다고 합시다. libbase가 쓰이지도 않는 Bar 를 파괴적으로 바꾸면 SemVer 규칙상 메이저 버전을 올려야 하고, 그러면 SAT 솔버는 2.x를 거부합니다. 실제로는 아무 문제 없이 돌아가는데도 말이죠. SemVer 검사만 무시하면 멀쩡한 조합을, 솔버가 ‘설치 불가’라고 막아서는 거짓 음성입니다.
확실하지 않은 약속일 수 있다(거짓 양성)
변경 자체만으로는 파괴적인지 아닌지 판단할 수 없다
‘어떻게 쓰이고 있는가’라는 맥락이 있어야 비로소 파괴적인지 판단됩니다. 시간에 민감한 API에 1ms 지연이 추가되면? 로그 출력 포맷이 바뀌면? ‘정렬되지 않은’ 스트림의 출력 순서가 바뀌면? 구문도 그대로고 약속한 기능도 그대로지만, 하이럼의 법칙에 따라 누군가는 그 동작에 의존하고 있습니다. 이론상 ‘안전한’ 패치가 실제로는 클라이언트를 깨뜨리고, 이론상 ‘파괴적인’ 제거가 실제로는 안전할 수 있습니다.
그래서 liba가 libbase 1.1이 아니라 libbase > 1.1.14가 필요하다고 명시할 수 있다는 것 자체가, 패치 번호 변경에도 사용자 입장의 차이가 포함될 수 있음 을 SemVer 시스템 스스로 시인하는 셈입니다. 결국 SAT 솔버가 “문제없다”고 해도 실제로는 빌드 에러나 심각한 런타임 버그로 나타날 수 있습니다.
버전업 동기가 어긋난다
파괴적 변경으로 수많은 사용자가 불편을 겪어도, 메인테이너는 그 비용 중 작은 일부만 부담 합니다(대부분이 간접 사용자라 변경을 알 길도 없습니다). 심지어 레거시 제약을 떨치고 인터페이스를 깔끔히 재설계하려면 호환성을 깨는 게 메인테이너에게 이익일 때도 있습니다. 보상 구조가 어긋나 있는 것입니다. 또 SemVer 버전업은 구문뿐 아니라 의미 변경 에도 이뤄져야 하는데, 의도적 의미 변경을 잡아주는 도구는 만들 수 없어 결국 사람이 최종 판단합니다 → 사람의 실수가 스며듭니다. (Go와 클로저는 메이저 버전업을 아예 새 패키지로 취급해, 호환성을 포기할 거면 이름까지 재포장하라고 요구함으로써 이 문제에 잘 대처합니다.)
최소 버전 선택(MVS)이라는 보완
러스 콕스가 제안한 SemVer의 변형입니다. 일반 SAT 솔버가 가능한 한 최신 버전을 고르는 것과 반대로, MVS는 요구된 최소 버전 을 고릅니다. liba 1.0이 libbase ≥ 1.7을 요구하면, 1.8이 있어도 1.7부터 시도합니다.
왜 이게 더 안전할까요? liba 1.0이 ≥ 1.7을 요구한다는 건 liba 개발자의 시스템에 1.7이 깔려 있었다는 뜻 이고, 릴리스 전 기본 테스트를 했다면 최소한 liba 1.0 + libbase 1.7의 상호운용은 이미 검증된 셈입니다. 즉 MVS는 ‘작성자가 실제로 테스트한 조합’에 가장 가까운 버전을 골라 충실성을 높입니다. 한 시간 치 작업을 커밋하는 게 일 년 치를 한꺼번에 커밋하는 것보다 안전하듯, 의존성도 차이가 작게 버전업하는 게 안전 하다는 발상입니다. 근본 문제(추정·보상)를 해결하진 못하지만, SemVer 방식의 신뢰도를 한 단계 끌어올립니다.
자원이 무한하다면 — 추정에서 증거로
사고 실험을 해봅니다. 컴퓨팅 자원이 무한하다면 의존성 관리는 어떤 모습일까요? 업계가 SemVer에 의지하는 이유는 ① 내부 정보만 있으면 되고, ② 충분한 테스트 자원·CI가 없어도 되며, ③ 관행이기 때문입니다. 그런데 ②는 향후 10년 안에 반드시 달라질 조건이고, 정보(다운스트림 사용 방식)는 OSS 생태계에서는 이미 공개되어 있습니다.
SemVer는 '있는 정보를 무시하기로 한 선택'이다
SemVer에는 ‘내 생각에 이 변경은 수월할(혹은 어려울) 것’이라는 메인테이너의 의도 가 담겨 있습니다. 그런데 이 의도를 전달할 더 나은 방법이 있습니다 — 받아들이기 수월한 변경임을 보여주는 실제적 경험(증거) 입니다. 다운스트림 대부분이 공개되어 있다면, 변경할 때마다 그들의 테스트를 돌려 ‘통계적으로 안전한지’를 직접 확인할 수 있습니다. API가 달라졌든 버그 수정이든 따로 분류·추정할 이유가 사라집니다.
완벽함(‘그 누구도 깨뜨리지 않기’)보다 ‘위험 줄이기’ 를 목표로 하면 비용은 훨씬 낮아집니다. 많이 쓰이는·잘 관리되는·유용한 테스트 결과를 제공해온 의존성을 우선 선정하고, 변경 위험도에 따라 테스트 범위를 조절하는 프리서브밋 분석 이 가능합니다. 12장의 변경 4분류(순수 리팩터링 ~ 동작 변경)를 테스트 수준에 매핑하는 식이죠.
다만 OSS 생태계가 여기 도달하려면 갈 길이 멉니다 — 모든 의존성이 단위 테스트를 제공해야 하고, ‘무엇이 이 프로젝트에 의존하는가’(역방향)까지 인덱싱돼야 하며, CI 자원이 충분해야 하고, 버전을 고정해두지 말아야 합니다. 그리고 어느 버전까지 검사할지를 따지면 결국 ‘현시점의 안정 버전을 테스트하라’ = 트렁크 기반 개발 = 헤드에서 지내기 모델로 수렴 합니다. 즉 자원이 무한할 때의 이상적 의존성 관리는 헤드에서 지내기와 같은 곳을 가리킵니다.
의존성 내보내기 — 공개의 장기 비용
지금까지는 가져오는 이야기였지만, 반대로 내가 의존성을 제공하는 쪽 일 때도 비용이 발생합니다. 선의로 오픈 소스를 공개했는데 오히려 조직에 해가 되는 두 가지 경로가 있습니다 — ① 품질·관리 부실로 평판이 떨어지거나(‘코드보다 커뮤니티’), ② 동기화를 유지하지 못해 포크 하나하나가 비용이 되는 경우입니다.
gflags 오픈 소스화 — '담벼락 너머로 던지기'의 교훈
2006년경 구글은 명령줄 플래그 라이브러리를 오픈 소스로 공개했습니다. 선한 행동처럼 보였지만, 당시 구글은 대규모 리팩터링 능력이 없어 내부 프로젝트를 OSS 버전으로 옮기지 못했고, 외부 기여 코드를 내부로 다시 통합할 수도 없었습니다. 결국 OSS 버전은 내부 버전과 단절된 채 방치되었고, 시간이 지나며 둘의 차이는 벌어졌습니다. 핵심 교훈: ‘장기 지원 계획(혹은 의무) 없이는 릴리스하지 말라.’ 게다가 나중에 내부 flags의 ‘문서화되지 않은 부분’을 수정하자, 그 동작에 의존하던 코드들이 무너지며(하이럼의 법칙) 수천 CPU를 절약할 최적화가 지연되었습니다.
AppEngine — 하이럼의 법칙이 비즈니스를 막은 사례
AppEngine은 원래 32비트 파이썬을 썼는데, 2014년 파이썬 런타임·C++ 컴파일러·표준 라이브러리를 동시에 교체하는 대규모 업그레이드를 했습니다. 대부분은 문제없었지만, 돈을 내는 외부 고객 중 일부가 최신 파이썬으로의 전환이나 늘어난 자원 소비를 감당하지 못했습니다. 그래서 비즈니스 이유로 강제 전환을 미뤄야 했고, AppEngine에서 시작하는 전이 의존성 전체가 옛 컴파일러·표준 라이브러리와도 호환되어야 하는 상황이 거의 3년 지속됐습니다. 버그 수정·성능 최적화조차 양쪽 버전 모두와 호환되어야 했죠.
여기서 보편적 교훈이 나옵니다. 사용자가 충분히 많으면 시스템의 모든 관찰 가능한 동작에 누군가는 의존합니다. 버전 관리가 의존성 관리로 바뀌는 순간, 코드가 어떻게 쓰이는지 확인할 길이 막히고, 외부 그룹(특히 비용을 내는 고객)과 우선순위를 다퉈야 하며, 순수한 엔지니어링 트레이드오프만 고민하던 행복한 시절이 끝납니다. 외부 사용자는 내부 사용자보다 관리 비용이 훨씬 크고, 외부에 공개한 의존성은 날이 갈수록 비용이 더 빠르게 증가 합니다. API를 릴리스하지 말라는 게 아니라, 릴리스를 결정할 때 장기적 위험을 반드시 계산에 넣으라는 뜻입니다.
비교 / 트레이드오프
네 가지 의존성 관리 모델
| 모델 | 시간·변경 처리 | 버전 선택 주체 | 핵심 비용·한계 |
|---|---|---|---|
| 변경 불가 | 변경 자체를 금지 | 불필요(버전 하나) | 오래 살아남는 프로젝트엔 비현실적, 조기 경보 없음 |
| SemVer | 사람이 위험을 번호로 추정 | SAT 솔버 | 정보 손실 → 과잉 구속(의존성 지옥)·과소 구속 |
| 하나로 묶어 배포 | 배포자가 호환 조합을 동결 | 전문 배포자 | 배포 시점에 고정, 개별 최신화가 어려움 |
| 헤드에서 지내기 | 제공자가 커밋 전 생태계를 검증 | 항상 최신(안정 버전) | 제공자 책임 폭증, 전 생태계 테스트·CI 필요 |
추정(SemVer) vs 증거(테스트·CI)
| 관점 | SemVer (추정) | 테스트·CI (증거) |
|---|---|---|
| 호환성 판단 근거 | 메인테이너가 자기 API만 보고 매긴 번호 | 다운스트림이 실제로 돌려본 결과 |
| 하이럼의 법칙 | 무력 — ‘내 API’만으론 증명 불가 | 실제 사용 방식이 테스트에 반영됨 |
| 정보 손실 | 변경을 번호로 압축하며 손실 | 손실 없음(영향받는 테스트가 직접 답함) |
| 규모 확장성 | 네트워크 커질수록 거짓 양성·음성 누적 | 컴퓨팅 자원만 받쳐주면 충실성 유지 |
규모가 작고 철저히 관리되는 소수 의존성만 쓴다면 SemVer는 완벽에 가깝습니다. 하지만 그래프가 커질수록(각 의존성이 커지거나, 의존성 수가 늘거나, 같은 외부 네트워크를 공유하는 모노리포) SemVer의 충실성 손실이 복합적으로 누적되어 혼란을 퍼뜨립니다.
내 생각
-
‘의존성 관리 → 버전 관리’는 BOM·내부 미러로 실천. Maven/Gradle의 BOM으로 버전을 한 곳에 고정하고, 사내 Nexus/Artifactory에 외부 라이브러리를 미러링하는 게 곧 ‘통제 불가를 통제 가능으로 끌어내리기’입니다. 의존성을 우리 빌드 파이프라인 안에 두는 순간 다이아몬드 문제가 추적 가능해집니다.
-
lockfile = ‘하나로 묶어 배포하기’의 축소판.
package-lock.json·go.sum·Pipfile.lock은 전이 의존성까지 특정 조합으로 동결해 재현 가능한 빌드를 만듭니다. 배포자 역할을 락파일이 대신하는 셈입니다. -
Dependabot/Renovate = ‘헤드에서 지내기’의 다운스트림 자동화 흉내. 제공자가 마이그레이션 도구를 주는 대신, 소비자가 봇으로 작게·자주 업그레이드하고 CI로 검증합니다. MVS의 ‘작게 버전업이 안전하다’와 ‘CI로 증거 확인’을 합친 현실적 절충입니다.
-
하이럼의 법칙은 공개 API 버저닝의 핵심 리스크.
@Deprecated표시만으로는 부족합니다. 외부 클라이언트가 우리가 약속하지 않은 응답 필드 순서·null 처리·타이밍에 의존하기 시작하면, ‘패치’조차 파괴적 변경이 됩니다. 그래서 SemVer 번호보다 컨슈머 주도 계약 테스트(Pact 등)가 더 정직한 신호입니다. -
외부 공개는 ‘코드 기여’가 아니라 ‘장기 계약’. gflags·AppEngine 사례의 교훈은 명확합니다 — 사내 라이브러리를 오픈소스화하거나 외부 API를 열기 전에 ‘EOL·지원 주체·동기화 비용’을 먼저 못 박아야 합니다. 책임 주체 없는 릴리스는 방치되고, 방치된 의존성은 보안 부채로 돌아옵니다.
관련 개념
- Ch16 버전 관리와 브랜치 관리 — ‘원-버전 규칙’과 트렁크 기반 개발이 이 장의 ‘버전 관리로 끌어내리기’·‘헤드에서 지내기’의 직접적 토대입니다. 다이아몬드 의존성도 이미 다룹니다
- Ch01 소프트웨어 엔지니어링이란 — ‘프로그래밍 + 시간 = 소프트웨어 엔지니어링’, 그리고 SemVer를 무너뜨리는 하이럼의 법칙의 출처입니다
- Ch15 폐기 — 의존성을 안전하게 업그레이드·제거하려면 결국 구버전 폐기가 필요합니다. 헤드에서 지내기의 ‘다운스트림 먼저 마이그레이션’과 맞물립니다
- Ch12 단위 테스트 · Ch11 테스트 개요 — 변경 위험도에 따라 테스트 범위를 고르는 ‘증거 기반 의존성 관리’의 전제이며, 변경 4분류의 출처입니다
- Ch14 더 큰 테스트 — 전이 의존성·CI를 통한 실제 통합 검증이 ‘추정 대신 증거’의 실행 형태입니다