✍️ 필사 모드: 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.
설계 목표:
- 분산: 모든 clone이 완전한 리포지토리. 중앙 서버 불필요.
- 빠름: 커널 크기의 리포지토리에서도 초 단위 작동.
- 무결성: 데이터 손상 즉시 감지.
- 동시 개발 지원: 브랜치/머지가 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는:
- 입력 내용에 헤더 추가.
- SHA-1 계산.
- zlib 압축.
.git/objects/3b/18e5.../에 저장.- 해시 출력.
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
이 명령이 하는 일:
- file.txt 읽기.
- Blob 해시 계산 (
blob <size>\0<content>SHA-1). .git/objects/해시 경로에 이미 있는지?- 있음: skip.
- 없음: zlib 압축해서 저장.
- Index 업데이트 (blob 해시 기록).
6.2 커밋 생성
git commit -m "Add feature"
- Index의 파일들로 tree 객체들 생성 (재귀적으로 디렉토리마다).
- 최상위 tree 해시.
- Commit 객체 생성: tree + parent + author + message.
- Commit 해시 계산.
HEAD가 가리키는 ref 업데이트.
6.3 Checkout
git checkout main
refs/heads/main의 해시 읽기.- 그 commit의 tree 읽기.
- Tree 순회하며 working directory 갱신.
- Index도 tree와 일치하도록 업데이트.
- 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이 하는 일:
main의 최신 commit을 임시 HEAD로.branch의 commits를 순서대로D,E나열.- 각 commit을
cherry-pick:- Ancestor 계산 (이전 commit).
- 3-way merge 수행.
- 새 commit 생성 (새 parent).
- 모든 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 루틴
"아, 실수했다" 순간:
- 당황하지 말 것. Reflog가 살려줄 가능성이 높다.
git reflog실행.- 복구하려는 지점 해시 확인.
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로) │
│ │
핵심:
- Refs 교환.
- "have" / "want" 협상.
- 서버가 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. 퀴즈
Q1. Git의 4가지 객체 타입은 무엇이며 어떻게 연결되는가?
A. blob(파일 내용), tree(디렉토리 목록), commit(스냅샷 + 메타), tag(annotated 태그). 연결 구조: commit이 최상위 tree를 가리키고, tree가 하위 blob과 tree들을 가리킨다. Commit은 또한 parent commit(들)을 가리켜 이력 DAG를 형성. 모든 연결은 SHA-1 해시다. 핵심: tree는 재귀적 — 디렉토리의 디렉토리는 tree 안에 tree 엔트리로 표현. 같은 내용의 blob은 단 하나만 저장된다(중복 제거). 한 byte만 바꿔도 해시 체인 전체가 연쇄적으로 변경 → 무결성 보장.
Q2. Packfile의 델타 압축은 어떻게 작동하는가?
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 단위였을 것.
Q3. Index(스테이징 에어리어)가 왜 존재하는가?
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는 "다음 커밋이 될 상태"를 명시적으로 만드는 레이어.
Q4. Rebase가 public 브랜치에서 위험한 이유는?
A. Rebase는 커밋의 해시를 변경한다. 같은 변경 내용이라도 parent가 바뀌면 SHA-1이 완전히 달라짐 → 실질적으로 새 커밋. 다른 개발자가 옛 해시 기반으로 이미 작업하고 있었다면 그 사람의 브랜치가 "사라진" 커밋에 의존하는 상황이 된다. git pull이 충돌 또는 중복 커밋 생성. --force push로 밀어버리면 옛 해시가 remote에서 지워져 상황 더 악화. 해결 원칙: "한 명만 보는 브랜치"에서만 rebase. 공유 브랜치는 merge로 합쳐야 이력이 보존. --force-with-lease는 "원격이 내가 본 상태 그대로일 때만" 조건을 걸어 최소한의 안전망.
Q5. Reflog가 실수 복구의 안전망인 이유는?
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`가 실행되기 전까진 살아있다.
Q6. Partial clone (--filter=blob:none)과 Shallow clone의 차이는?
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 리포가 이 방식으로 운영된다.
Q7. Ort 알고리즘이 recursive를 대체한 이유는?
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/564)
- **Git은 content-addressable 파일시스템이다**. 버전 관리는 그 위에 구축된 레이어에 불과. 모든 데이터(파일, 디렉토리, 커밋)는 SHA-1 해시로 주소 ...