필사 모드: Git Internals Deep Dive — Object Model, Packfile, Merge 알고리즘, Reflog, 프로토콜 완전 정복 (2025)
한국어TL;DR
- **Git은 content-addressable 파일시스템이다**. 버전 관리는 그 위에 구축된 레이어에 불과. 모든 데이터(파일, 디렉토리, 커밋)는 SHA-1 해시로 주소 지정.
- **4가지 객체 타입**: **blob**(파일 내용), **tree**(디렉토리), **commit**(스냅샷 + 메타), **tag**(서명된 참조).
- **Loose object** vs **Packfile**: 처음엔 각 객체가 파일 하나(`.git/objects/ab/cd...`), 시간이 지나면 packfile로 압축하여 델타 저장.
- **Refs**: 브랜치/태그는 단순히 commit 해시를 가리키는 텍스트 파일. `HEAD`는 현재 체크아웃된 것.
- **Index**: 스테이징 에어리어. 다음 커밋의 tree를 미리 구성하는 바이너리 파일.
- **Merge**: 2021년 `recursive` → **`ort`** 기본 알고리즘 변경. 500배 빠른 경우도.
- **Rebase**: 커밋을 하나씩 cherry-pick해 새 base 위에 재배치. 커밋 해시 변경 주의.
- **Reflog**: HEAD와 refs의 모든 변경 이력. "잃어버린 커밋"을 복구하는 비밀.
- **Packfile**: 델타 압축 + 인덱스(`.idx`) + 비트맵(`.bitmap`). Git push/fetch를 KB 수준으로 줄임.
- **Protocol**: Smart HTTP (most common), SSH, Git protocol. 클라이언트가 "내가 가진 것"을 선언 → 서버가 필요한 것만 전송.
1. Git의 철학 — "파일시스템, 버전 관리 아님"
1.1 Linus Torvalds의 2주
2005년, BitKeeper(Linux 커널이 쓰던 VCS)가 무료 라이선스를 철회했다. Linus는 분노했고 2주 안에 대체제를 만들어냈다. **Git**.
설계 목표:
1. **분산**: 모든 clone이 완전한 리포지토리. 중앙 서버 불필요.
2. **빠름**: 커널 크기의 리포지토리에서도 초 단위 작동.
3. **무결성**: 데이터 손상 즉시 감지.
4. **동시 개발 지원**: 브랜치/머지가 first-class.
Linus의 관점: **기존 VCS들은 틀린 추상화를 썼다**. 파일 간 diff를 저장하는 대신 **스냅샷**을 저장해야 한다. Git의 핵심 아이디어는 이것이다.
1.2 Content-Addressable Filesystem
Git의 본질은 **키-값 저장소**:
SHA-1 해시 → 객체 내용
저장: "이 내용을 저장해줘" → Git이 SHA-1 해시를 돌려줌.
조회: "이 해시에 해당하는 내용을 줘" → Git이 객체를 돌려줌.
이것이 전부다. 나머지(브랜치, 머지, 히스토리)는 이 위에 구축된 레이어다.
1.3 간단한 실험
mkdir /tmp/gitrepo && cd /tmp/gitrepo
git init
echo "Hello, Git" | git hash-object -w --stdin
3b18e512dba79e4c8300dd08aeb37f8e728b8dad
`hash-object -w`는:
1. 입력 내용에 헤더 추가.
2. SHA-1 계산.
3. zlib 압축.
4. `.git/objects/3b/18e5.../` 에 저장.
5. 해시 출력.
git cat-file -p 3b18e512
Hello, Git
"이 해시의 내용을 보여줘" → 내용 반환. **버전 관리 없이** 내용 기반 저장소가 작동.
2. 객체 모델
2.1 4가지 객체 타입
**Blob**: 파일 내용 (메타 없음). 같은 내용 두 파일은 같은 blob.
**Tree**: 디렉토리 목록. "이 이름 → 이 모드 → 이 blob/tree".
**Commit**: 스냅샷 (tree 포인터) + 메타 (부모, 저자, 메시지).
**Tag**: annotated 태그. Commit을 가리키는 서명/메시지 포함 객체.
2.2 Blob 상세
Blob은 순수한 내용:
blob <byte_length>\0<content>
예: "Hello, Git" (11바이트, 개행 포함):
blob 11\0Hello, Git\n
이것을 SHA-1 → 식별자. zlib으로 압축해 저장.
**같은 내용 = 같은 blob**. 10,000 파일이 같은 "Hello World"를 가지면 blob 하나만 저장.
2.3 Tree 상세
Tree는 디렉토리:
tree <length>\0
100644 blob abc123... README.md
040000 tree def456... src
100755 blob ghi789... build.sh
각 엔트리:
- **모드**: `100644`(일반 파일), `100755`(실행), `040000`(디렉토리), `120000`(심볼릭 링크), `160000`(서브모듈).
- **타입**: blob 또는 tree.
- **SHA-1**: 20 바이트 바이너리.
- **이름**: null로 끝.
git cat-file -p HEAD^{tree}
100644 blob a906cb2... README.md
040000 tree 68aba62... src
100755 blob 9daeafb... build.sh
Tree는 파일시스템 스냅샷을 재귀적으로 표현한다. 현재 디렉토리는 tree 하나, 하위 디렉토리는 tree의 tree.
2.4 Commit 상세
Commit은 tree를 가리키는 포인터 + 메타데이터:
commit <length>\0
tree 3c4e9cd789...
parent 5a9d8b41...
author Linus Torvalds <torvalds@linux.org> 1234567890 -0700
committer Linus Torvalds <torvalds@linux.org> 1234567890 -0700
Initial commit
필드:
- **tree**: 이 커밋의 전체 파일시스템 스냅샷.
- **parent**: 부모 커밋(들). 첫 커밋은 없음. 머지 커밋은 2개 이상.
- **author**: 변경을 만든 사람 + 시간.
- **committer**: 커밋을 적용한 사람 + 시간. 보통 author와 같음. rebase/cherry-pick 시 다름.
- **메시지**: 빈 줄 이후 텍스트.
git cat-file -p HEAD
tree 3c4e9cd789...
parent 5a9d8b41...
author John Doe <john@example.com> 1700000000 +0000
committer John Doe <john@example.com> 1700000000 +0000
#
Add feature X
2.5 Tag 상세
Annotated tag (`git tag -a`):
tag <length>\0
object 3c4e9cd789...
type commit
tag v1.0.0
tagger John Doe <john@example.com> 1700000000 +0000
Release v1.0.0
-----BEGIN PGP SIGNATURE-----
...
-----END PGP SIGNATURE-----
Lightweight tag (`git tag`)는 그냥 ref (아래 설명).
2.6 해시 체인
네 타입이 연결된 그래프:
commit → tree → blob
→ blob
→ tree → blob
commit → parent commit → parent commit → ...
모든 연결이 **해시**. 한 바이트만 바뀌어도 해시가 완전히 달라진다 → 모든 조상 커밋의 해시도 연쇄적으로 변경.
이것이 Git의 **무결성 보장**이다. 한 부분을 변조하면 이후 모든 해시가 틀리므로 즉시 감지.
2.7 SHA-1 vs SHA-256
Git은 원래 SHA-1을 썼다. 2017년 Google이 **SHA-1 충돌 공격**(SHAttered)을 증명한 후 SHA-256 지원 추가.
- 2020+: `git init --object-format=sha256` 옵션.
- SHA-1과 SHA-256은 **상호 호환 불가**. 별도 저장소.
- 대부분 프로젝트는 여전히 SHA-1. 마이그레이션 복잡.
2.8 Loose Object의 저장
새로 만든 객체는 **loose object**로 저장:
.git/objects/
├── 3b/
│ └── 18e512dba79e4c8300dd08aeb37f8e728b8dad (첫 2자 디렉토리, 나머지 38자 파일)
├── 5a/
│ └── 9d8b41...
└── ...
파일 내용: `<type> <size>\0<content>` 를 zlib으로 압축. 해시는 압축 전 내용의 SHA-1.
왜 첫 2자로 디렉토리를 나누는가? 많은 파일시스템이 한 디렉토리의 파일 수가 수만을 넘으면 느려지기 때문. 256 버킷으로 분산.
3. Refs — 의미 있는 이름
3.1 Ref = 해시의 별칭
브랜치, 태그, HEAD는 단순히 **해시를 가리키는 파일**이다.
cat .git/refs/heads/main
3c4e9cd789abc...
**그게 전부다**. "main 브랜치"는 이 파일 하나. 아무 커밋 해시로 내용을 바꾸면 브랜치가 그 커밋을 가리킨다.
3.2 HEAD
현재 체크아웃된 ref:
cat .git/HEAD
ref: refs/heads/main
**Detached HEAD**는 branch 대신 직접 해시:
cat .git/HEAD
3c4e9cd789abc...
`git checkout <commit_hash>`가 이 상태를 만든다.
3.3 Tag refs
cat .git/refs/tags/v1.0.0
3c4e9cd789abc...
Lightweight tag: commit을 바로 가리킴.
Annotated tag: tag object를 가리킴, 그 object가 commit을 가리킴.
3.4 Remote refs
ls .git/refs/remotes/origin/
main develop feature-x
cat .git/refs/remotes/origin/main
3c4e9cd789abc...
Remote에서 마지막으로 fetch한 상태. 로컬 브랜치와 완전히 독립.
3.5 packed-refs
수만 개 ref가 있으면 파일이 너무 많음. Git은 주기적으로 ref들을 하나의 파일로 packing:
cat .git/packed-refs
3c4e9cd789abc refs/heads/main
5a9d8b41... refs/tags/v1.0.0
...
개별 파일은 "unpacked" 상태. Git이 ref 조회 시 둘 다 확인(unpacked 우선).
3.6 Symbolic Ref
Ref가 다른 ref를 가리킬 수 있다. HEAD가 대표적:
HEAD → refs/heads/main → <commit_hash>
`git symbolic-ref HEAD`로 직접 볼 수 있다.
4. Index — 스테이징 에어리어
4.1 역할
Index는 **다음 커밋의 tree를 미리 구성**하는 캐시다. `git add`가 업데이트하는 대상.
작업 디렉토리 → (git add) → Index → (git commit) → Commit
4.2 구조
`.git/index`는 **바이너리 파일**:
header (magic + version + count)
entries:
- ctime, mtime
- dev, ino (inode)
- mode
- uid, gid
- file size
- SHA-1 (blob)
- flags
- path name
checksum
각 항목은 이미 `git add`된 파일을 가리킨다.
git ls-files --stage
100644 a906cb2a4a904a15... 0 README.md
100644 3b18e512dba79e4c... 0 src/main.c
4.3 왜 index가 있는가
**이유 1: 부분 커밋**. `git add` 일부 파일만 → 선택적 커밋.
**이유 2: 성능**. 파일 변경 감지가 inode/size만 비교로 빠름. 실제 diff 비싸지 않음.
**이유 3: 머지 중간 상태**. 충돌 시 index에 여러 단계(stage)로 저장:
100644 <ancestor_hash> 1 foo.c # 공통 조상
100644 <ours_hash> 2 foo.c # 우리 쪽
100644 <theirs_hash> 3 foo.c # 저쪽
4.4 "3 트리" 모델
Git의 세 가지 상태:
HEAD Index Working Tree
(last commit) → (staged) → (unstaged)
↓ ↓ ↓
commit objects index file filesystem
`git status`는 두 쌍을 비교:
- **HEAD vs Index**: "Changes to be committed".
- **Index vs Working Tree**: "Changes not staged".
5. Packfiles — 효율적 저장
5.1 Loose Object의 한계
10,000 파일에 10번씩 수정하면 loose object 100,000개. 각각이 파일이라 inode 낭비 + 디스크 공간 낭비.
더 큰 문제: **델타 압축 불가**. 100MB 파일의 10바이트 수정이 100MB 새 blob 생성.
5.2 Packfile의 해결
Packfile은 **델타 압축된 객체 묶음**:
.git/objects/pack/
├── pack-abc123.idx # 인덱스 (offset lookup)
├── pack-abc123.pack # 실제 데이터
├── pack-abc123.bitmap # reachability bitmap (선택)
5.3 Pack 포맷
Pack file:
헤더: "PACK" + version + object count
객체들:
- 타입 (blob/tree/commit/tag/delta)
- 압축된 데이터
- OFS_DELTA 또는 REF_DELTA 면 base 참조
체크섬 (20 바이트)
5.4 델타 압축
객체 A가 객체 B와 비슷하면:
Pack의 B: 전체 내용 (zlib 압축)
Pack의 A: "B를 참조해서, 다음 edit instructions 적용하라"
Edit instructions:
- **COPY**: "B의 offset X부터 N 바이트".
- **INSERT**: "이 새 N 바이트 삽입".
예: "Hello, World" → "Hello, Git" 변경:
A = COPY(0, 7) + INSERT("Git") + COPY(12, 1)
= "Hello, " + "Git" + "\n"
대부분의 파일 변경은 작은 edit → 델타가 훨씬 작다.
5.5 Base Object 선택
어느 객체를 base로 쓸지? Git의 휴리스틱:
- **같은 파일명의 이전 버전**.
- **크기가 비슷한 객체**.
- **접두사 매칭**.
`git repack -f` 옵션으로 강제 재구성 가능.
5.6 Delta Chain
델타가 또 다른 델타의 base가 될 수 있다:
V1 (full) ← V2 (delta of V1) ← V3 (delta of V2) ← ...
검색 시 체인을 거슬러 올라가며 복원. 너무 긴 체인은 성능 저하 → `pack.depth` (기본 50)로 제한.
5.7 Pack Index (.idx)
`.pack` 파일 자체는 순차. 특정 해시를 찾으려면 `.idx` 사용:
Pack index:
fanout table (256 entries, 해시 첫 바이트별 오프셋)
sorted sha1 list
CRC32 list
offset list
빠른 lookup: fanout → binary search. O(log n).
5.8 Reachability Bitmap
Git 2.0+. 각 커밋에 **도달 가능한 객체들의 bitmap**을 저장:
Commit abc123:
bitmap: 0b101100110011... (각 비트가 pack의 객체)
`git fetch`가 "이 커밋이 필요하지만 그 부모들은 이미 있어"를 빠르게 계산. GitHub의 `git clone`이 빨라진 비결.
6. Object Storage 동작
6.1 객체 생성
git add file.txt
이 명령이 하는 일:
1. file.txt 읽기.
2. Blob 해시 계산 (`blob <size>\0<content>` SHA-1).
3. `.git/objects/` 해시 경로에 이미 있는지?
- 있음: skip.
- 없음: zlib 압축해서 저장.
4. Index 업데이트 (blob 해시 기록).
6.2 커밋 생성
git commit -m "Add feature"
1. Index의 파일들로 **tree 객체들 생성** (재귀적으로 디렉토리마다).
2. 최상위 tree 해시.
3. Commit 객체 생성: tree + parent + author + message.
4. Commit 해시 계산.
5. `HEAD`가 가리키는 ref 업데이트.
6.3 Checkout
git checkout main
1. `refs/heads/main`의 해시 읽기.
2. 그 commit의 tree 읽기.
3. Tree 순회하며 working directory 갱신.
4. Index도 tree와 일치하도록 업데이트.
5. HEAD를 main으로 갱신.
6.4 GC (Garbage Collection)
git gc
- Loose object → packfile로 압축.
- Unreachable 객체 (어떤 ref/reflog에서도 도달 불가) 삭제.
- 기본 2주 이전 dangling 객체 제거.
자동 GC는 `.git/objects`에 loose object가 6700+ 되면 트리거.
6.5 Prune
Reachable object만 유지하고 나머지 영구 삭제:
git prune --expire=now
주의: reflog의 entries도 유지 이유가 된다. Reflog가 clean이어야 실제 삭제.
7. 머지 알고리즘
7.1 3-way Merge 기본
두 브랜치를 합칠 때:
- **Common Ancestor**: 두 브랜치의 공통 조상 커밋.
- **Ours**: 현재 브랜치의 마지막 커밋.
- **Theirs**: 합칠 브랜치의 마지막 커밋.
Git은 각 파일에 대해:
Ancestor (base)
↓ ↓
Ours Theirs
**Case 1**: Ours만 변경 → Ours 사용.
**Case 2**: Theirs만 변경 → Theirs 사용.
**Case 3**: 둘 다 같은 변경 → 그 변경 사용.
**Case 4**: 둘 다 다른 변경 → **충돌**.
7.2 Fast-forward
가장 단순한 케이스:
main: A → B → C
branch: A → B → C → D → E
`git merge branch`하면 main을 E로 그냥 이동. 머지 커밋 없음. "Fast forward".
7.3 3-way Merge Commit
main: A → B → C → D
branch: A → B → E → F
공통 조상 B. 머지 결과:
main: A → B → C → D → M
↑
branch: A → B → E → F → ┘
M은 **머지 커밋**: 두 개 부모를 가진다.
7.4 Recursive → Ort
2021년까지 Git의 기본 merge 알고리즘은 **recursive**. 공통 조상이 여러 개인 경우(criss-cross merge), 조상들을 재귀적으로 merge.
문제: 대형 레포에서 극도로 느릴 수 있음. "Too many changes" 케이스에서 디스크 사용량 폭발.
**Ort** (Ours/Recursive/Theirs): Elijah Newren이 다시 쓴 알고리즘.
- **500x 빠른 경우도**.
- **Rename detection** 개선.
- **Subtree merge** 지원.
- **메모리 효율** 개선.
2021년 `git 2.33`부터 기본. 대부분 개발자는 변경을 눈치채지 못함 — merge가 그냥 빨라졌다.
7.5 Rename Detection
Before: foo.c
After: bar.c (내용 비슷)
Git은 이를 **rename**으로 인식한다 (내용 유사도가 50%+ 이면 기본). 중요한 이유: rename 된 파일에 다른 브랜치에서 편집이 있었다면 merge가 **이전 이름에 적용**할 게 아니라 **새 이름에 적용**해야.
Ort는 rename detection 더 정확 + 빠름.
7.6 충돌 해결
git merge branch
Auto-merging foo.c
CONFLICT (content): Merge conflict in foo.c
`foo.c` 내용:
<<<<<<< HEAD
int x = 1;
=======
int x = 2;
>>>>>>> branch
개발자가 편집 후:
git add foo.c
git commit # merge commit 생성
Index의 다중 stage entries가 사용된다.
7.7 Strategy 옵션
- **`-X ours`**: 충돌 시 항상 Ours 선택.
- **`-X theirs`**: 항상 Theirs.
- **`-X ignore-all-space`**: 공백 무시.
- **`-X rename-threshold=80`**: rename 감지 임계.
전략 자체 (`-s`):
- `ort`: 기본, 2-way + criss-cross 모두.
- `recursive`: Legacy, 같은 역할.
- `resolve`: 간단, 역사적.
- `octopus`: 여러 브랜치 한 번에 (충돌 없는 경우만).
- `ours`: Theirs를 완전히 무시 (이력만 합침).
8. Rebase 메카닉
8.1 아이디어
main: A → B → C
branch: A → B → D → E
`git rebase main`:
main: A → B → C
branch: A → B → C → D' → E'
`D'`와 `E'`는 **새 commit**. 같은 변경이지만 다른 parent → 다른 해시.
8.2 실제 동작
내부적으로 Git이 하는 일:
1. `main`의 최신 commit을 임시 HEAD로.
2. `branch`의 commits를 순서대로 `D`, `E` 나열.
3. 각 commit을 `cherry-pick`:
- Ancestor 계산 (이전 commit).
- 3-way merge 수행.
- 새 commit 생성 (새 parent).
4. 모든 commit이 성공하면 `branch`를 새 HEAD로 업데이트.
8.3 Interactive Rebase
git rebase -i HEAD~5
텍스트 에디터:
pick abc123 Commit 1
pick def456 Commit 2
squash ghi789 Commit 3
reword jkl012 Commit 4
drop mno345 Commit 5
Actions:
- **pick**: 그대로 유지.
- **reword**: 커밋 메시지 변경.
- **edit**: 중간에 멈추고 수정 가능.
- **squash**: 이전 커밋에 합침.
- **fixup**: squash + 이전 메시지 유지.
- **drop**: 삭제.
이력을 **다시 쓴다** (history rewriting). 새 해시 생성.
8.4 Rebase의 위험
**Public 브랜치에 rebase하지 말 것**:
나 (local)
git rebase main
푸시
git push --force # <-- 위험
다른 사람이 옛 해시 기반으로 일하고 있으면 그 사람의 작업이 꼬인다. Public 브랜치에는 **merge 사용**.
8.5 Force-with-lease
`--force`의 안전한 버전:
git push --force-with-lease
"원격이 내가 마지막으로 본 상태와 같을 때만 force push". 다른 사람이 push한 게 있으면 거부.
9. Reflog — 안전망
9.1 Reflog란
**HEAD와 각 ref의 변경 이력**을 기록하는 로컬 로그. `git reset --hard`, `git rebase` 같은 "위험한" 작업 후에도 복구 가능.
git reflog
abc123 (HEAD -> main) HEAD@{0}: commit: Add feature
def456 HEAD@{1}: commit: Fix bug
ghi789 HEAD@{2}: rebase finished: returning to refs/heads/main
jkl012 HEAD@{3}: rebase: onto main
...
9.2 복구 예제
**시나리오**: `git reset --hard`로 실수로 최근 커밋 날림.
git reflog
abc123 HEAD@{1}: commit: 잃어버린 커밋!
def456 HEAD@{0}: reset: moving to HEAD~1
git reset --hard abc123
복구됨
Git이 reset했지만 객체는 아직 `.git/objects/`에 있다. Reflog에 해시가 기록돼 있으므로 찾을 수 있다.
9.3 저장 위치
cat .git/logs/HEAD
각 ref별로 별도 로그:
.git/logs/HEAD
.git/logs/refs/heads/main
.git/logs/refs/heads/feature
9.4 만료
Reflog는 영원하지 않다. 기본 설정:
- **reachable 객체**: 90일 후 reflog entry 제거.
- **unreachable**: 30일 후.
`git gc` 시 정리.
9.5 Recovery 루틴
"아, 실수했다" 순간:
1. **당황하지 말 것**. Reflog가 살려줄 가능성이 높다.
2. `git reflog` 실행.
3. 복구하려는 지점 해시 확인.
4. `git reset --hard <hash>` 또는 `git cherry-pick <hash>`.
30일 안에만 기억하면 거의 모든 실수 복구 가능.
10. Partial Clone과 Sparse Checkout
10.1 큰 리포의 문제
Linux 커널: **3GB**. Chromium: **50GB**. 완전 clone은 시간/공간 낭비.
10.2 Shallow Clone
git clone --depth=1 https://github.com/...
최근 commit 1개만. 이력 없음. CI/CD에 자주 사용.
단점: 전체 이력 필요할 때 `git pull` 불가.
10.3 Partial Clone
Git 2.19+. 객체의 **일부만** 받기:
git clone --filter=blob:none https://github.com/...
- 모든 commit + tree 받음.
- Blob은 lazy하게 필요할 때 받음.
- `git log`는 빠름.
- `git checkout`은 느릴 수 있음 (blob 다운로드).
10.4 Sparse Checkout
워킹 디렉토리에 **일부 디렉토리만** 펼치기:
git sparse-checkout init
git sparse-checkout set src/feature-x docs
`src/feature-x`와 `docs`만 체크아웃. 거대 monorepo에서 필수. Facebook, Google 같은 기업이 활용.
10.5 조합
git clone --filter=blob:none --sparse https://...
git sparse-checkout init
git sparse-checkout set src/mymodule
- 필요한 blob만.
- 필요한 디렉토리만.
- 50GB 리포가 1GB 이하로.
10.6 Git LFS (Large File Storage)
또 다른 접근: 큰 파일을 **별도 서버**에 저장, git에는 포인터만.
.git/hooks에 LFS 필터 설치
→ 1GB 파일 대신 60바이트 포인터 저장
→ 실제 파일은 LFS 서버에서 다운로드
이미지, 영상, 빌드 결과물에 적합. 정상 diff 불가는 단점.
11. Git 프로토콜
11.1 Clone/Fetch 흐름
Client Server
│ │
│ "내 refs는 [A, B, C]야" ────────────►│
│ │
│◄──────────── "내 refs는 [D, E, F]야" │
│ │
│ "D, E, F가 필요해. 난 A, B가 있어" ──►│
│ │
│ ◄──── packfile (D, E, F에 필요한 │
│ 객체만, A/B 기반 delta로) │
│ │
핵심:
1. Refs 교환.
2. "have" / "want" 협상.
3. 서버가 packfile 생성 → 전송.
11.2 Smart HTTP
가장 흔한 방식 (GitHub, GitLab).
GET /repo.git/info/refs?service=git-upload-pack
POST /repo.git/git-upload-pack
HTTP 위에 Git 고유 protocol. Firewall 친화적 (port 443).
11.3 SSH
ssh git@github.com "git-upload-pack 'user/repo.git'"
SSH 세션 위에 같은 protocol. 더 빠른 경우가 있다 (HTTP 오버헤드 없음).
11.4 Git Protocol (port 9418)
원본 git protocol. 인증 없음(익명 clone만). 거의 사용 안 됨.
11.5 Protocol V2 (2018+)
개선된 버전. `capabilities^{}` 협상, 더 효율적:
- **ls-refs** 필터링: 원하는 ref만 요청.
- **압축 개선**.
- **Stateless**: 각 요청이 독립.
GitHub/GitLab에서 기본. Git 2.18+.
11.6 Packfile 전송 최적화
서버는 **reachability bitmap**을 사용해 필요한 객체를 빠르게 계산.
Client: "wants [D]"
Server: D + parents + trees + blobs 를 bitmap로 계산
Server: 이미 Client가 가진 것은 제외
Server: Delta 압축해서 pack 생성
GitHub 같은 대형 호스팅은 pre-computed packfile + 사용자별 차이만 실시간 압축.
12. 디버깅과 탐색 도구
12.1 git cat-file
객체 내용 확인:
git cat-file -t abc123 # type
git cat-file -s abc123 # size
git cat-file -p abc123 # pretty-print
git cat-file --batch-all-objects --unordered # 모든 객체 나열
12.2 git rev-list
Reachable commit 순회:
git rev-list HEAD # HEAD에서 도달 가능한 모든 commit
git rev-list --count HEAD # commit 개수
git rev-list --objects HEAD # commit + tree + blob 해시들
12.3 git verify-pack
Packfile 내용 분석:
git verify-pack -v .git/objects/pack/pack-abc123.idx
각 객체의 type, size, 압축 크기, 델타 체인 등
12.4 git fsck
무결성 검사:
git fsck --full
dangling blob (레퍼런스 없는 blob)
dangling commit (도달 불가)
missing object (파손!)
12.5 git show-ref
모든 ref:
git show-ref
abc123 refs/heads/main
def456 refs/heads/feature
ghi789 refs/tags/v1.0.0
...
12.6 git count-objects
저장소 통계:
git count-objects -v
count: 3 ← loose objects
size: 12 ← KB
in-pack: 5000 ← packed objects
packs: 1
size-pack: 5000 ← KB
13. 흔한 실전 시나리오
13.1 "실수로 force push 했어요"
git reflog # 원래 상태 해시 찾기
git push --force-with-lease origin main:<원래 해시>
Reflog가 기록한 옛 상태로 돌아가기.
13.2 "brick wall 충돌"
대형 머지에서 수백 개 충돌:
git merge --abort # 일단 취소
더 작은 단위로 나누기
git checkout feature
git rebase -i main # 커밋을 정리
git checkout main
git merge feature # 이제 충돌이 적음
13.3 "커밋을 둘로 나누고 싶어요"
git rebase -i HEAD~3
해당 커밋을 'edit'으로 표시
rebase가 그 커밋에서 멈춤
git reset HEAD^ # 변경을 unstage
git add file1
git commit -m "첫 번째 부분"
git add file2
git commit -m "두 번째 부분"
git rebase --continue
13.4 "민감 정보를 실수로 커밋했어요"
최근 커밋이면
git reset --soft HEAD~1
파일 제거, 재커밋
오래전 커밋이면
git filter-branch --index-filter \
'git rm --cached --ignore-unmatch secrets.txt' \
HEAD
또는 BFG Repo-Cleaner 사용 (훨씬 빠름)
주의: force push 필요, 모든 clone이 영향받음. 비밀번호는 이미 노출됐다고 보고 **즉시 교체**.
13.5 "이 파일이 언제 추가됐지?"
git log --all --full-history --source -- <file>
git log --diff-filter=A -- <file> # 추가 커밋만
git log -p -- <file> # 각 변경의 diff
git blame <file> # 줄별 최종 변경자
14. Git 2024-2025의 변화
14.1 git worktree 개선
여러 브랜치를 동시에 체크아웃:
git worktree add ../feature-x feature-x
../feature-x 디렉토리에서 feature-x 브랜치 작업
여러 폴더에서 동시에 작업 가능. Context switching 없이 여러 PR 검토.
14.2 Reftable
많은 ref가 있는 리포에서 `refs/` 디렉토리가 느려진다. **Reftable**은 단일 파일 포맷:
- O(log n) lookup.
- 효율적 업데이트.
- 2024년 실험적, 2025년 안정화 중.
14.3 Scalar
Microsoft의 대형 리포 최적화 도구. Git 2.38+에 통합:
scalar clone https://github.com/microsoft/windows.git
Partial clone + sparse checkout + background maintenance 자동 설정.
14.4 SSH Signing
GPG 대신 SSH 키로 커밋 서명:
git config gpg.format ssh
git config user.signingkey ~/.ssh/id_ed25519.pub
git commit -S -m "Signed"
GPG 세팅의 복잡함 없이 서명 가능. GitHub도 2022+ 지원.
15. 학습 리소스
**책**:
- "Pro Git" — Scott Chacon (공식 무료 책). https://git-scm.com/book
- "Git in Practice" — Mike McQuaid.
- "Git Internals" — Peepcode (얇지만 깊음).
**온라인**:
- https://git-scm.com/docs — 공식 reference.
- https://learngitbranching.js.org — 인터랙티브 튜토리얼.
- "A Hacker's Guide to Git" — Wildly Inaccurate 블로그.
**영상**:
- "Git From the Bits Up" — Tim Berglund.
- Linus Torvalds 2007 Google Tech Talk (역사적 가치).
**실습**:
- `.git/` 디렉토리 탐험.
- `git cat-file`로 객체 직접 읽기.
- 토이 Git 구현해보기.
**논문**:
- Jean-Philippe Aumasson et al. "SHAttered" (SHA-1 충돌).
- Git mailing list 아카이브 (새 기능 논의).
16. 요약 — 한 장 정리
┌─────────────────────────────────────────────────────┐
│ Git Internals Cheat Sheet │
├─────────────────────────────────────────────────────┤
│ 본질: │
│ Content-addressable filesystem │
│ SHA-1 해시 → 객체 │
│ │
│ 객체 타입: │
│ blob : 파일 내용 │
│ tree : 디렉토리 │
│ commit : 스냅샷 + 메타 │
│ tag : annotated 태그 │
│ │
│ 저장: │
│ Loose: .git/objects/ab/cd... (개별 파일) │
│ Pack: .git/objects/pack/*.pack (델타 압축) │
│ Index: .idx (빠른 lookup) │
│ Bitmap: reachability 가속 │
│ │
│ Refs: │
│ refs/heads/<name> = 브랜치 │
│ refs/tags/<name> = 태그 │
│ refs/remotes/origin/<name> = remote 상태 │
│ HEAD = 현재 │
│ packed-refs = 최적화 │
│ │
│ Index: │
│ .git/index 바이너리 │
│ staging area │
│ 3트리 모델: HEAD / Index / Working │
│ │
│ Merge: │
│ 3-way: ancestor + ours + theirs │
│ Fast-forward: 선형 이력 │
│ Ort algorithm (2021+) │
│ Rename detection │
│ │
│ Rebase: │
│ Cherry-pick each commit │
│ 새 해시 생성 │
│ interactive로 편집 │
│ Public 브랜치에 쓰지 말 것 │
│ │
│ Reflog: │
│ 모든 ref 변경 기록 │
│ .git/logs/ │
│ 30-90일 유지 │
│ 실수 복구의 안전망 │
│ │
│ 대형 리포: │
│ --depth=1 (shallow) │
│ --filter=blob:none (partial) │
│ sparse-checkout │
│ LFS (바이너리) │
│ │
│ 프로토콜: │
│ Smart HTTP (기본) │
│ SSH │
│ Protocol V2 (2018+) │
│ │
│ 도구: │
│ cat-file, rev-list, fsck │
│ verify-pack, count-objects │
│ reflog, show-ref │
└─────────────────────────────────────────────────────┘
17. 퀴즈
**A.** **blob**(파일 내용), **tree**(디렉토리 목록), **commit**(스냅샷 + 메타), **tag**(annotated 태그). 연결 구조: commit이 최상위 tree를 가리키고, tree가 하위 blob과 tree들을 가리킨다. Commit은 또한 parent commit(들)을 가리켜 이력 DAG를 형성. 모든 연결은 SHA-1 해시다. 핵심: **tree는 재귀적** — 디렉토리의 디렉토리는 tree 안에 tree 엔트리로 표현. 같은 내용의 blob은 단 하나만 저장된다(중복 제거). 한 byte만 바꿔도 해시 체인 전체가 연쇄적으로 변경 → 무결성 보장.
**A.** Packfile은 비슷한 객체들을 **base + edit instructions** 형태로 저장한다. Base 객체는 그대로 (zlib 압축), 그와 비슷한 객체는 "base의 offset X부터 N 바이트 copy" + "이 새 M 바이트 insert" 같은 지시어로 표현. 결과: 100MB 파일의 10 바이트 수정이 10 바이트 delta로 저장(원본 100MB 대신). Base 선택은 휴리스틱(같은 파일명 이전 버전, 크기 유사도 등). 체인 길이는 기본 50으로 제한(너무 길면 복원 느려짐). 이 때문에 `git clone`이 KB 단위로 동작 가능. Loose object만 썼다면 `git clone`이 GB 단위였을 것.
**A.** 세 가지 이유: (1) **부분 커밋** — `git add file1`로 선택적 커밋 가능. 변경된 여러 파일 중 일부만 커밋하고 싶을 때 필수. (2) **성능** — Index는 각 파일의 inode, mtime, 크기를 캐시해서 `git status`가 실제 파일 diff를 돌리지 않고도 변경을 감지. 수만 파일 리포에서 밀리초 단위 응답. (3) **머지 중간 상태** — 충돌 시 index에 여러 stage(ancestor/ours/theirs)를 저장해 `git checkout --theirs file` 같은 명령을 가능하게 한다. "3-tree 모델"(HEAD, Index, Working)에서 Index는 "다음 커밋이 될 상태"를 명시적으로 만드는 레이어.
**A.** Rebase는 **커밋의 해시를 변경**한다. 같은 변경 내용이라도 parent가 바뀌면 SHA-1이 완전히 달라짐 → 실질적으로 **새 커밋**. 다른 개발자가 옛 해시 기반으로 이미 작업하고 있었다면 그 사람의 브랜치가 "사라진" 커밋에 의존하는 상황이 된다. `git pull`이 충돌 또는 중복 커밋 생성. `--force` push로 밀어버리면 옛 해시가 remote에서 지워져 상황 더 악화. 해결 원칙: **"한 명만 보는 브랜치"에서만 rebase**. 공유 브랜치는 merge로 합쳐야 이력이 보존. `--force-with-lease`는 "원격이 내가 본 상태 그대로일 때만" 조건을 걸어 최소한의 안전망.
**A.** Reflog는 **HEAD와 모든 ref의 변경 이력을 로컬에 기록**한다. `git reset --hard`, `git rebase`, `git checkout`, `git commit` 등 모든 ref 변경이 타임스탬프와 함께 `.git/logs/` 아래에 저장. 중요한 것: **"객체는 바로 삭제되지 않는다"** — `git reset`으로 커밋이 현재 ref에서 사라져도 reflog에 해시가 남아있으면 객체는 그대로 `.git/objects/` 에 존재. 따라서 `git reflog`로 옛 해시 찾아 `git reset --hard <hash>`로 복구 가능. 기본 30-90일 보관(reachable/unreachable). 이 때문에 Git에서 "완전히 잃어버렸다"는 경우가 거의 없다. 'git gc`가 실행되기 전까진 살아있다.
**A.** **Shallow clone** (`--depth=N`)은 최근 N개 commit만 받는다. 이력이 잘려있어 `git log`가 제한적이고 일부 `git pull`이 작동 안 함. 과거 커밋을 못 봄. CI/CD에 주로 사용. **Partial clone** (`--filter=blob:none`)은 **모든 commit과 tree는 받지만 blob은 lazy하게** 받는다. `git log` 완전히 가능, 이력 탐색 OK. `git checkout`이나 `git show`로 특정 파일 내용이 필요할 때만 서버에서 blob 다운로드. 대형 repo를 경량으로 다루는 더 유연한 방법. Sparse checkout과 조합하면 50GB 리포가 1GB 이하로 작동. Microsoft Windows 리포가 이 방식으로 운영된다.
**A.** **성능과 정확성** 두 가지. Recursive는 criss-cross merge(공통 조상이 여러 개인 상황)에서 조상들을 재귀적으로 merge하는데, 이 과정이 대형 레포에서 지수적으로 느려질 수 있었다. 극단적 케이스에서 merge가 시간 초과되거나 메모리 폭발. Ort(Ours/Recursive/Theirs)는 Elijah Newren이 2021년 다시 작성한 알고리즘으로: (1) **500배 빠른 경우도** 있고, (2) **rename detection이 정확**(파일 이동 후 편집 처리), (3) **메모리 효율** 대폭 개선. Git 2.33(2021)부터 기본. 대부분 개발자는 변경을 눈치채지 못했다 — merge가 그냥 빨라졌다. "사용자가 눈치채지 못하는 좋은 변화"의 전형적 예.
이 글이 도움이 됐다면 다음 포스트도 확인해 보세요:
- "Binary Serialization Protobuf/Thrift/Avro/FlatBuffers" — Git의 바이너리 포맷과 비교.
- "Docker BuildKit & Image Layers Deep Dive" — 또 다른 content-addressable 시스템.
- "RocksDB & LSM-Tree Deep Dive" — append-only + background compaction 철학.
- "Consistent Hashing & Virtual Nodes" — content addressing의 분산 버전.
현재 단락 (1/543)
- **Git은 content-addressable 파일시스템이다**. 버전 관리는 그 위에 구축된 레이어에 불과. 모든 데이터(파일, 디렉토리, 커밋)는 SHA-1 해시로 주소 ...