Git— 스냅샷으로 이해하는 버전 관리
2장

브랜치와 HEAD

브랜치는 커밋 하나를 가리키는 41바이트짜리 파일이다 — 그래서 생성과 전환이 순식간에 일어난다.

브랜치의 진짜 정체

많은 개발자가 브랜치를 "코드의 복사본"이라고 오해한다. 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 파일에 저장된 특수 포인터다. 평상시에는 브랜치를 가리키는 심볼릭 레퍼런스로 작동한다.

.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로 바뀐다. 커밋 그래프 자체는 전혀 변경되지 않는다.

Attached HEAD — HEAD가 브랜치 이름을 통해 커밋을 간접 참조하는 상태. git commit을 하면 브랜치가 자동으로 앞으로 이동한다. 일반적인 작업 상태다.
Detached HEAD — HEAD가 브랜치가 아닌 특정 커밋 SHA를 직접 가리키는 상태. git checkout <sha>, git checkout v1.0(태그), git rebase 내부 동작 등에서 발생한다. 이 상태에서 커밋을 해도 어떤 브랜치도 업데이트되지 않으므로 주의가 필요하다.

브랜치 생성과 전환의 전체 흐름

구체적인 예를 통해 브랜치 생성, 전환, 커밋 후 두 브랜치가 발산하는 과정을 추적해 보자.

브랜치 생성과 HEAD 이동 1 / 6
SPACE 재생 · ← → 이동 · R 리셋
함정: git branch feature는 브랜치를 만들지만 HEAD를 옮기지 않는다. 계속 main에 커밋하게 되는 흔한 실수다. git switch -c feature는 브랜치 생성과 전환을 한 번에 처리한다.

Detached HEAD — 언제 생기고 어떻게 빠져나오나

Detached HEAD는 오류가 아니다. 의도적으로 과거 커밋을 탐색하거나 태그 시점의 코드를 실험할 때 유용한 상태다. 하지만 이 상태에서 커밋한 뒤 브랜치로 돌아오면 그 커밋들이 "고아"가 된다.

Detached HEAD에서 작업하다 새 브랜치가 필요하다고 깨달았다면 언제든지 git switch -c <새브랜치>를 실행하면 된다. 그러면 현재 커밋 위에 새 브랜치가 생성되고 HEAD가 그 브랜치에 붙는다(attached). 기존에 만든 커밋들도 모두 새 브랜치에 안전하게 포함된다.

Detached HEAD 상황과 탈출
# 태그 v1.0 시점 코드를 보고 싶을 때 — detached HEAD 발생
git checkout v1.0

# 이 상태에서 실험 커밋을 했다면
git switch -c experiment/v1-hotfix

# 또는 그냥 돌아가려면 (실험 커밋 포기)
git switch main
핵심: Detached HEAD에서 git switch main으로 돌아오면 그동안 만든 커밋에는 더 이상 어떤 브랜치도 가리키지 않는다. GC가 실행되기 전까지 SHA를 알면 접근할 수 있지만, 결국 수거된다. 실험 커밋을 보관하려면 반드시 브랜치를 만들어 두자.

원격 추적 브랜치

로컬 브랜치 외에 origin/main 같은 이름의 브랜치를 본 적이 있을 것이다. 이것이 원격 추적 브랜치다.

git fetchgit pull을 실행하면 Git은 원격 저장소의 브랜치 상태를 로컬의 .git/refs/remotes/origin/ 아래에 복사한다. origin/main은 "마지막으로 origin과 통신했을 때 origin의 main이 가리키던 커밋"을 기록하는 읽기 전용 스냅샷이다.

이 원격 추적 브랜치는 직접 커밋할 수 없다. 항상 로컬 브랜치를 통해 작업하고, git push로 원격에 반영하는 방식으로 동기화한다. origin/main과 로컬 main의 차이가 "아직 push하지 않은 커밋 수"이고, git status의 "ahead 2, behind 1" 메시지가 그것을 알려준다.

업스트림 추적 (tracking)git push -u origin main-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  # 이름 변경
← 스냅샷과 커밋병합 →