Git Branch는 왜 이렇게 가벼운가
41바이트 텍스트 파일이 branch의 전부인 이유부터 switch의 3단계 갱신, tracking 설정, 명명 충돌까지 — Git branch 설계 철학을 추적한다.
- 01 Git이 파일이 아닌 SHA로 세상을 보는 이유
- 02 Git의 모든 포인터는 어디에 살고 있는가
- 03 Git은 파일 변경을 어떻게 추적하는가
- 04 Git history는 왜 DAG인가
- 05 Git Branch는 왜 이렇게 가벼운가
- 06 Git merge는 어떻게 충돌을 판단하는가
- 07 Rebase는 왜 새 commit을 만드는가
- 08 Git에서 변경을 되돌린다는 것의 의미
- 09 git stash는 어떻게 두 영역을 동시에 보존하는가
- 10 git push/fetch는 내부에서 무슨 일이 일어나는가
- 11 Git은 어떻게 실수를 복구하는가
- 12 Git Hook은 어디서 막고 어디서 통과시키는가
- 13 Submodule과 Subtree — 외부 레포 통합의 두 가지 철학
- 14 거대 레포는 어떻게 git을 견디는가
- 15 Git 워크플로우의 선택은 왜 팀 규모와 신뢰 수준에 달려 있는가
- 16 Git이 Push를 거부할 때 — 7가지 거부의 구조
Git에서 branch를 만드는 데 걸리는 시간은 수 밀리초다. SVN이나 Perforce에서 branch가 무거운 작업인 것과 대조적이다. 왜 이렇게 가벼운가? 그리고 이 가벼움이 어떤 제약을 함께 만들어내는가?
Branch의 본질 — 41바이트 파일
Git에서 branch는 정확히 .git/refs/heads/<name> 위치에 있는 텍스트 파일 하나다. 내용은 40자 hex SHA와 줄바꿈 1자, 합계 41바이트가 전부다.
$ cat .git/refs/heads/main
abc1234567890abcdef1234567890abcdef123456
$ wc -c .git/refs/heads/main
41
branch를 만드는 것은 이 파일을 생성하는 것이고, 삭제는 파일을 지우는 것이다. git branch feature는 내부적으로 git update-ref refs/heads/feature HEAD와 동등하다. branch에는 작성자도, 생성 시간도, 설명도 없다. 생성 기록은 reflog에, 설명은 .git/config의 branch.<name>.description에 따로 존재한다.
이 단순함이 “branching is cheap”이라는 말의 근거다. 1,000개 branch를 만들어도 .git/refs/heads의 용량은 약 50KB에 불과하다. git pack-refs --all을 실행하면 loose ref 파일들이 .git/packed-refs 단일 파일로 압축되어 inode 부담도 사라진다.
git switch가 하는 세 가지 일
branch가 파일 하나라면, branch 이동은 그 파일을 바꾸는 것처럼 들린다. 실제로는 세 영역을 동시에 갱신한다.
git switch feature 단계:
1. .git/HEAD → "ref: refs/heads/feature"
2. .git/index → feature가 가리키는 commit의 tree
3. Working directory → target tree에 맞게 파일 갱신
핵심은 3단계가 변경된 파일만 건드린다는 점이다. main과 feature가 공유하는 파일은 다시 쓰지 않는다. 그래서 같은 commit을 가리키는 branch 간 이동은 거의 즉각적이다.
working directory에 commit되지 않은 변경이 있을 때 Git의 판단은 명확하다.
- target branch에서 그 파일이 동일하다 → 변경을 보존하며 이동
- target branch에서 그 파일이 다르다 → 이동 거부 (“would be overwritten”)
충돌이 없으면 Git은 사용자의 작업을 지우지 않는다. 충돌 가능성이 있을 때만 멈춘다. git stash 또는 git commit 후 다시 시도하면 된다.
git switch는 Git 2.23에서 등장했다. 이전의 git checkout은 branch 이동과 파일 복원을 한 명령으로 처리해 모호했다. 이제 branch 이동은 switch, 파일 복원은 restore로 역할이 분리됐다.
Tracking — “어디로 push할 것인가”
.git/config의 두 줄이 tracking의 전부다.
[branch "feature"]
remote = origin
merge = refs/heads/feature
이 설정이 있으면 git push, git pull, git status가 인자 없이 동작한다. git push -u origin feature는 push와 동시에 이 두 줄을 config에 기록한다.
git status의 “ahead 2, behind 1” 표시는 이 설정으로 계산된다. 내부적으로 git rev-list --left-right --count origin/feature...HEAD와 동등하다. merge-base를 기준으로 로컬에만 있는 commit이 ahead, upstream에만 있는 commit이 behind다. 둘 다 0보다 크면 “diverged”다.
git config --global push.default simple
git config --global pull.rebase true
git config --global pull.ff onlypush.default simple은 upstream과 이름이 다르면 push를 거부해 의도치 않은 배포를 막는다. pull.rebase true는 fetch 후 merge commit 대신 rebase로 linear history를 유지한다.
명명 충돌 — 파일시스템의 제약
branch 이름이 파일 경로라는 사실은 미묘한 제약을 만든다. feature/foo와 feature/foo/bar를 동시에 만들 수 없다.
$ git branch feature/foo
$ git branch feature/foo/bar
fatal: cannot lock ref 'refs/heads/feature/foo/bar':
'refs/heads/feature/foo' exists; cannot create 'refs/heads/feature/foo/bar'
feature/foo는 파일이고, feature/foo/bar는 그 파일을 디렉토리로 사용하려는 시도다. 파일시스템에서 같은 경로가 파일과 디렉토리를 동시에 될 수 없다. git pack-refs --all로 packed-refs로 이동해도 Git은 양쪽 모두에서 이 검사를 수행한다.
git check-ref-format --branch <name>으로 이름의 유효성을 사전 검증할 수 있다. 이 외에도 공백, ~, ^, :, ?, *, [, \, 연속 점(..), .lock 접미사, @{ 시퀀스 등이 금지된다.
트레이드오프
가벼움의 대가: branch가 파일 하나이기 때문에 생성·삭제는 수 밀리초지만, 수만 개 ref는 inode 부담을 준다(packed-refs로 완화). 파일 경로가 branch 이름이기 때문에 계층 구조 명명이 가능하지만 feature/a와 feature/a/b 충돌이 생긴다. git branch -d의 reachable 검사는 unmerged commit 손실을 막지만, -D로 강제 삭제 후 reflog 만료가 지나면 복구 불가다.
git branch -d는 upstream 또는 HEAD에 merge되지 않은 commit이 있으면 거부한다. 이는 실수를 막는 안전장치다. -D는 이 검사를 건너뛰고 ref, reflog, config를 모두 삭제한다. 객체 자체는 GC 전까지 살아있어 git reflog에서 SHA를 찾아 복구할 수 있다.
git branch -m old new(rename)는 ref 갱신뿐 아니라 reflog 파일 이동과 .git/config의 [branch "old"] 섹션을 [branch "new"]로 함께 이동한다. upstream 설정이 보존된다. -c(copy)는 ref와 reflog만 복제하고 config는 새 branch에 별도 설정을 요구한다.
정리
- Branch는
.git/refs/heads/<name>파일 하나, SHA 40자 + 줄바꿈이 전부다. git switch는 HEAD ref → Index → Working 순으로 세 영역을 갱신하며, 변경된 파일만 건드린다.- Tracking은
branch.<name>.remote/merge두 줄로, ahead/behind 계산과 인자 없는 push/pull의 기반이다. - Branch 이름은 파일 경로이므로
feature/a와feature/a/b는 공존 불가 — 명명 컨벤션에서 사전에 결정해야 한다.
이 설계의 근본은 “ref는 SHA를 가리키는 포인터일 뿐”이라는 단순함이다. 그 단순함 위에 tracking, 명명 규칙, reflog 같은 층이 쌓여 협업 워크플로우가 만들어진다.