TECH 으로 돌아가기
TECH HACKER NEWS 오늘 9분 읽기 29 READS

깃 서브모듈을 패키지 매니저로 볼 때 드러나는 균열

깃 서브모듈을 패키지 매니저로 볼 때 드러나는 균열
SOURCE IMAGE · HACKER NEWS

깃 서브모듈은 오래전부터 "왜 이렇게 쓰기 불편한가"라는 질문을 반복적으로 낳아온 기능이다. 그런데 이 불편함을 막연한 감정이 아니라 구조적으로 뜯어보면, 서브모듈이 사실상 패키지 매니저의 역할을 어설프게 흉내 내고 있다는 사실이 드러난다. 상위 프로젝트(superproject) 트리에 mode 160000으로 기록되는 gitlink, 즉 특정 경로에 박아 둔 커밋 SHA는 락파일의 한 항목에 해당한다. 경로와 원격 URL을 매핑하는 .gitmodules 파일은 매니페스트다. git submodule update는 이 둘을 읽어 워킹 트리를 채우는데, 이것이 곧 설치 단계다. 핀 자체의 정밀도는 어느 패키지 매니저에도 뒤지지 않는다. 객체 ID로 지목되는 정확한 커밋이기 때문이다. 대부분의 조각이 이미 존재하고 동작도 대체로 들어맞지만, 완전히 정렬되지 않은 채 사용 경험만 거의 모든 단계에서 나빠져 있다는 점이 문제의 핵심이다.

매니페스트가 호스트를 하드코딩하는 대가

가장 근본적인 간극은 해석(resolution)에 있다. gitlink는 어떤 커밋을 체크아웃할지만 기록할 뿐이므로, 실제로 어디서 그 커밋을 가져올지는 .gitmodules에 적힌 URL이 유일한 통로다. 문제는 커밋 SHA가 객체에 대한 호스트 독립적 정체성인데도, 깃에는 그 SHA를 가진 서버를 찾아 주는 조회 수단이 전혀 없다는 점이다. 그래서 상위 저장소가 이름을 바꾸거나, 다른 호스트로 이전되거나, 비공개로 전환되면 SHA가 그대로이고 이미 클론된 모든 사본에 객체가 남아 있어도 하위의 핀은 전부 깨진다. 게다가 git submodule init이 처음 실행될 때 각 URL은 상위 저장소의 .git/config에 submodule..url로 복사되고, 이후 명령들은 .gitmodules가 아니라 이 값을 읽는다. 커밋된 .gitmodules를 미러나 포크로 수정해도 이미 초기화된 클론은 git submodule sync를 돌리기 전까지 바뀌지 않는다. CI에서 흔히 쓰는 우회책이 url..insteadOf 설정으로, HTTPS 주소를 SSH로 바꿔 배포 키를 적용하거나 내부 호스트명을 미러로 돌리는 식이다. 원격 URL을 매니페스트가 붙박아 두는 구조 자체가 이런 임기응변을 강제한다.

워크트리와 부딪히는 저장소 레이아웃

서브모듈의 깃 디렉터리는 상위 저장소의 $GIT_DIR/modules// 아래에 저장되고, 서브모듈 워킹 트리에는 그곳을 가리키는 gitdir 포인터가 담긴 .git 파일이 놓인다. 각 항목은 자체 refs, HEAD, 인덱스, 설정, 훅, 그리고 기본적으로 자체 객체 저장소까지 갖춘 독립된 깃 디렉터리다. 그래서 서브모듈 하나를 제거하는 일조차 세 곳에 흩어진다. git rm으로 gitlink와 .gitmodules 항목을 지우고, git submodule deinit으로 워킹 트리와 .git/config 항목을 비운 뒤, 두 명령이 모두 남겨 두는 $GIT_DIR/modules/ 디렉터리는 문서상 수동 rm -rf로 지워야 한다. 이 레이아웃이 워크트리와 정면충돌한다. 링크된 워크트리는 상위의 $GIT_DIR을 공유하면서도 자체 워킹 트리·HEAD·인덱스를 갖기 때문에, 서로 다른 브랜치에 놓인 두 워크트리는 같은 서브모듈을 두 커밋에서 참조하게 된다. 실제로 필자는 지난주 워크트리를 하나 붙여 브랜치를 시험하다가, 정리하려 한 git worktree remove가 거부당하는 것을 겪었다. 매뉴얼상 서브모듈을 포함한 워크트리는 --force가 필요하고, git worktree move는 아예 거부한다. 자기 기능 두 개가 충돌해 스스로 --force를 요구하는 셈이다.

이 문제의 역사는 짧지 않다. 깃 2.5가 워크트리를 도입한 2015년 7월 발표에는 이미 "서브모듈을 포함한 저장소에서는 권장하지 않는다"는 한 줄 경고가 붙어 있었고, 11년이 지난 지금도 상황은 그대로다. 그사이 worktree add는 submodule.recurse를 무시하도록 패치돼야 했는데, 새 워크트리에서 비어 있는 서브모듈 경로로 reset --hard가 재귀해 들어가는 문제 때문이었다. 올해 3월 Xavier Morel이 깃 메일링 리스트에 서브모듈 체크아웃을 기존 공유 클론의 워크트리로 삼을 수 있는지 물었고, 4월에는 worktree add에 --recurse-submodules를 붙여 각 워크트리가 하드링크로 저장소를 공유하는 자체 서브모듈 깃 디렉터리를 갖게 하는 RFC와 패치 시리즈가 이어졌다.

캐시도, 버전 범위도, 안전한 파싱도 없다

같은 중복은 단일 워크트리 클론에서도 나타난다. 두 서브모듈이 세 번째 저장소를 공유해도 깃은 이를 무관한 체크아웃으로 취급해 각 경로마다 별도 modules/ 항목과 별도 객체 저장소를 만든다. cargo의 레지스트리 캐시, pnpm의 콘텐츠 주소 저장소, Go 모듈 캐시처럼 바이트를 한 번만 저장하고 위치마다 꺼내 쓰는 공유 캐시가 서브모듈에는 없다. 버전 표현력도 빈약하다. gitlink는 커밋 하나뿐이라 앞으로 옮기려면 서브모듈에 들어가 fetch·checkout 후 상위에서 git add로 새 gitlink를 기록해야 한다. update --remote는 지정 브랜치의 끝을 따라가지만, 버전 범위나 태그 패턴, 최소 커밋 같은 문법은 없다. Dependabot과 Renovate가 gitlink를 올리는 PR을 열 수 있는 것도 결국 매니페스트가 노출하는 유일한 참조가 브랜치 이름뿐이기 때문이다.

보안 관점에서는 더 날카롭다. .gitmodules가 저장소에 커밋되므로 적대적 상위가 그 내용을 통제하고, clone --recurse-submodules는 사용자가 가져온 파일을 보기도 전에 이를 파싱한다. 이 조합은 원격 코드 실행을 여러 차례 낳았다. CVE-2018-11235는 서브모듈 이름에 ../를 넣어 깃 디렉터리를 바깥에 쓰고 훅을 실행시켰고, CVE-2018-17456은 URL을 -로 시작시켜 자식 clone이 이를 옵션으로 파싱하게 했다. CVE-2022-39253은 심링크로 로컬 파일을 복사하게 만든 결함으로, protocol.file.allow 기본값이 user로 바뀌는 계기가 됐다. 요컨대 패키지 매니저가 매니페스트 포맷·리졸버·로컬 캐시로 감싸 두는 것들을 서브모듈은 객체 ID, 분리된 HEAD, modules/ 레이아웃, 전송 URL까지 그대로 노출한다. 대부분의 간극은 패키지 매니저가 이미 해결한 영역이지만, 커밋 ID에서 서버로의 매핑이라는 해석 문제만큼은 여전히 열려 있다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://nesbitt.io/2026/09/01/git-submodules-as-a-package-ma...
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기
처리 중...