Skip to content

✍️ 필사 모드: Git Internals Deep Dive — Object Model, Packfile, Merge 알고리즘, Reflog, 프로토콜 완전 정복 (2025)

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

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년 recursiveort 기본 알고리즘 변경. 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:    ABC
branch:  ABCDE

git merge branch하면 main을 E로 그냥 이동. 머지 커밋 없음. "Fast forward".

7.3 3-way Merge Commit

main:    ABCD
branch:  ABEF

공통 조상 B. 머지 결과:

main:    ABCDM
branch:  ABEF → ┘

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:    ABC
branch:  ABDE

git rebase main:

main:    ABC
branch:  ABCD' → 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-xdocs만 체크아웃. 거대 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 (얇지만 깊음).

온라인:

영상:

  • "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 해시로 주소 ...

작성 글자: 0원문 글자: 19,382작성 단락: 0/564