LabHub
배우기 러닝패스 코스

Git 실전 · 내 컴퓨터에선 되는데요 · 이론

부모가 기억하는 것은 브랜치가 아니라 커밋 하나다

LabHub 에서 이어서 보기

한 줄 요약

서브모듈에서 부모 저장소가 기록하는 것은 커밋 해시 하나입니다. 브랜치가 아닙니다.
그래서 서브모듈은 항상 detached HEAD 로 체크아웃되고, 최신을 받아 쓴 사실을 부모에
커밋하지 않으면 그 변화는 나만의 것으로 남습니다.

왜 이게 필요했나

여러 서비스가 같은 코드를 나눠 쓰는 상황은 흔합니다. 사내 공통 라이브러리, 프로토콜
정의, 설정 템플릿 같은 것들입니다. 복사해 두면 금세 갈라지고, 패키지로 만들어 올리면
배포 절차가 하나 더 생깁니다. 서브모듈은 그 사이의 답입니다 — 다른 저장소를 내
저장소의 한 디렉터리 자리에 핀으로 꽂아 둡니다.

문제는 "핀으로 꽂는다" 가 무슨 뜻인지 정확히 모른 채 쓰기 시작한다는 데 있습니다.
많은 사람이 "부모가 그 라이브러리의 main 브랜치를 가리킨다" 고 생각하는데, 그렇지
않습니다. 부모의 트리에는 그 자리에 모드 160000 의 항목 하나가 들어가고, 값은
커밋 해시입니다. 브랜치 이름은 .gitmodules 에도 기본으로 적히지 않습니다.

$ git ls-tree HEAD vendor/lib160000 commit 9f3c1a4... 	vendor/lib

이 한 줄이 서브모듈의 전부입니다. 이것을 알면 뒤따르는 모든 동작이 설명됩니다.

어떻게 동작하나

.gitmodules어디서 받아오는지(url)와 어디에 두는지(path)만 적은 설정
파일이고, 추적되는 일반 파일입니다. 실제로 어떤 버전인지는 트리의 160000 항목이
정합니다. 둘은 역할이 다르고, 그래서 따로 커밋됩니다.

서브모듈 디렉터리 안으로 들어가 git status 를 하면 이렇게 나옵니다.

HEAD detached at 9f3c1a4

부모가 브랜치가 아니라 커밋을 기억하므로, 체크아웃도 커밋으로 됩니다. 여기서
그냥 커밋하면 어느 브랜치에도 붙지 않은 커밋이 되어 나중에 찾기 어려워집니다.
서브모듈에서 작업할 일이 있으면 먼저 브랜치를 잡아야 합니다.

git submodule status앞 글자 한 칸이 상태를 말합니다.

| 앞 글자 | 뜻 |
| --- | --- |
| (공백) | 부모가 기록한 커밋과 실제 체크아웃이 같다 |
| + | 서브모듈이 부모가 기록한 것과 다른 커밋에 있다 |
| - | 아직 초기화되지 않았다(디렉터리가 비어 있다) |
| U | 병합 충돌이 해결되지 않았다 |

- 는 특히 자주 만납니다. git clone 은 기본으로 서브모듈을 받아오지 않기 때문에,
받자마자 빌드하면 빈 디렉터리 때문에 실패합니다. git clone --recurse-submodules
로 받거나, 받은 뒤 git submodule update --init --recursive 를 합니다. 평소에
git config submodule.recurse true 를 켜 두면 checkout·pull 이 서브모듈까지
따라옵니다.

+ 는 사고의 씨앗입니다. 서브모듈에서 최신을 받아 쓰고 잘 돌아가는 것을 확인했는데,
부모에 git add vendor/lib 로 그 변화를 커밋하지 않으면 내 기계에서만 새 코드가
쓰입니다. 동료가 clone 하면 부모가 기록한 옛 커밋을 받습니다. 증상은 "내 컴퓨터에선
되는데요" 의 전형이고, 원인이 서브모듈이라는 것을 알아채기까지 오래 걸립니다.

한 가지 더. git 2.38.1 부터 서브모듈을 로컬 경로나 file:// 로 받아오는 것
기본으로 막혔습니다(CVE-2022-39253). 악의적인 저장소가 .gitmodules 로 남의 기계의
파일을 가져가게 만들 수 있었기 때문입니다. 로컬 경로로 실습하거나 CI 에서 캐시
저장소를 쓸 때는 -c protocol.file.allow=always 를 명시해야 합니다.

현장에서 만나는 모습

다음 실습에서 할 것

라이브러리 저장소와 부모 저장소를 직접 만들어 서브모듈로 꽂습니다. 로컬 경로가
기본으로 막히는 것을 먼저 만나고, 부모의 트리에 160000 항목이 들어가는 것을 눈으로
확인합니다. 그다음 라이브러리에 새 커밋을 쌓고 서브모듈만 옮긴 채 부모를 커밋하지
않은 상태에서 clone 을 떠, 동료가 옛 코드를 받는 사고를 그대로 재현합니다.

공식 문서는 [git-submodule](https://git-scm.com/docs/git-submodule) 과
[Pro Git 7.11 Submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules) 입니다.