브랜치의 진짜 정체
많은 개발자가 브랜치를 "코드의 복사본"이라고 오해한다. Git에서 브랜치는 파일 하나, 그 안에 40자리 SHA 문자열 하나가 전부다.
브랜치 파일은 .git/refs/heads/ 디렉터리 아래에 브랜치 이름과 동일한 파일명으로 저장된다. main 브랜치라면 .git/refs/heads/main 파일이 존재하며, 그 내용은 현재 브랜치가 가리키는 커밋의 SHA 40자리 + 개행 한 문자 = 41바이트다.
파일 하나를 복사·생성하는 것이 전부이므로 git branch feature는 파일 시스템 연산 한 번으로 완료된다. 수십만 개의 파일을 가진 저장소에서도 브랜치 생성은 수 밀리초면 충분하다.
# main 브랜치가 가리키는 SHA cat .git/refs/heads/main # 모든 로컬 브랜치 목록 (파일 이름 = 브랜치 이름) ls .git/refs/heads/ # git branch와 동일한 결과 git branch
HEAD — 현재 위치를 알려주는 나침반
수십 개의 브랜치 중 "지금 내가 작업하는 곳"을 Git이 어떻게 알까? 그 답이 HEAD다.
HEAD는 .git/HEAD 파일에 저장된 특수 포인터다. 평상시에는 브랜치를 가리키는 심볼릭 레퍼런스로 작동한다.
# 브랜치를 추적하는 일반 상태 (attached HEAD) ref: refs/heads/main # 특정 커밋을 직접 가리키는 상태 (detached HEAD) a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
HEAD가 브랜치를 가리킬 때, 새 커밋이 생성되면 두 가지 일이 동시에 일어난다. 브랜치 파일이 새 커밋의 SHA로 갱신되고, HEAD는 여전히 그 브랜치를 가리키므로 자동으로 새 커밋 위에 위치하게 된다.
git switch feature (또는 구형 git checkout feature)를 실행하면 .git/HEAD 파일의 내용이 ref: refs/heads/feature로 바뀐다. 커밋 그래프 자체는 전혀 변경되지 않는다.
브랜치 생성과 전환의 전체 흐름
구체적인 예를 통해 브랜치 생성, 전환, 커밋 후 두 브랜치가 발산하는 과정을 추적해 보자.
Detached HEAD — 언제 생기고 어떻게 빠져나오나
Detached HEAD는 오류가 아니다. 의도적으로 과거 커밋을 탐색하거나 태그 시점의 코드를 실험할 때 유용한 상태다. 하지만 이 상태에서 커밋한 뒤 브랜치로 돌아오면 그 커밋들이 "고아"가 된다.
Detached HEAD에서 작업하다 새 브랜치가 필요하다고 깨달았다면 언제든지 git switch -c <새브랜치>를 실행하면 된다. 그러면 현재 커밋 위에 새 브랜치가 생성되고 HEAD가 그 브랜치에 붙는다(attached). 기존에 만든 커밋들도 모두 새 브랜치에 안전하게 포함된다.
# 태그 v1.0 시점 코드를 보고 싶을 때 — detached HEAD 발생 git checkout v1.0 # 이 상태에서 실험 커밋을 했다면 git switch -c experiment/v1-hotfix # 또는 그냥 돌아가려면 (실험 커밋 포기) git switch main
원격 추적 브랜치
로컬 브랜치 외에 origin/main 같은 이름의 브랜치를 본 적이 있을 것이다. 이것이 원격 추적 브랜치다.
git fetch나 git pull을 실행하면 Git은 원격 저장소의 브랜치 상태를 로컬의 .git/refs/remotes/origin/ 아래에 복사한다. origin/main은 "마지막으로 origin과 통신했을 때 origin의 main이 가리키던 커밋"을 기록하는 읽기 전용 스냅샷이다.
이 원격 추적 브랜치는 직접 커밋할 수 없다. 항상 로컬 브랜치를 통해 작업하고, git push로 원격에 반영하는 방식으로 동기화한다. origin/main과 로컬 main의 차이가 "아직 push하지 않은 커밋 수"이고, git status의 "ahead 2, behind 1" 메시지가 그것을 알려준다.
-u 플래그는 로컬 main이 origin/main을 "업스트림"으로 추적하도록 설정한다. 이후 git pull / git push를 원격명 없이 사용할 수 있게 된다. 설정은 .git/config의 [branch "main"] 섹션에 저장된다.브랜치 네이밍과 삭제
브랜치 이름은 파일 시스템 경로이므로 몇 가지 제약이 있다. 실무에서 자주 쓰이는 네이밍 관례도 살펴보자.
슬래시(/)를 네임스페이스 구분자로 쓸 수 있다(feature/login, fix/auth-race). 이때 .git/refs/heads/feature/ 디렉터리가 생기므로 feature라는 이름의 파일과 feature/ 디렉터리가 동시에 존재할 수 없다는 제약이 생긴다. 즉, feature 브랜치와 feature/login 브랜치를 동시에 가질 수 없다.
브랜치 삭제는 단순히 해당 파일을 지우는 것이다. git branch -d feature는 파일만 삭제하며 커밋 그래프는 전혀 건드리지 않는다. 가리키던 커밋들은 다른 브랜치나 태그가 참조하지 않는 한 결국 GC에 의해 수거된다.
git branch # 로컬 브랜치 목록 git branch -a # 원격 추적 브랜치 포함 전체 목록 git branch feature # feature 브랜치 생성 (HEAD 이동 없음) git switch -c feature # 생성 + 전환 (권장) git switch main # main으로 전환 git branch -d feature # 병합된 브랜치 삭제 git branch -D feature # 강제 삭제 (미병합도 삭제) git branch -m old-name new-name # 이름 변경