Git 을 매일 쓰지만 git commit 이 실제로 .git 디렉토리에 무엇을 박는지는 잘 모른다. Git 의 천재성은 단순함 — 4 가지 object type 으로 모든 history, branch, merge, tag 를 표현. 이 가이드는 Git 의 내부 구조 — object, ref, pack file, SHA-1 content addressable — 그리고 merge / rebase / reset 가 실제로 무엇을 옮기는지 정리한다.
.git 디렉토리 — Git 의 데이터베이스
.git/
├── HEAD ← "지금 어디 있는지" (ref:refs/heads/main)
├── config ← repo 설정
├── objects/ ← 모든 데이터
│ ├── 8a/
│ │ └── b4f1... ← SHA-1 prefix 2 + remainder
│ ├── pack/ ← packed objects (gc 후)
│ │ ├── pack-xxx.idx
│ │ └── pack-xxx.pack
│ └── info/
├── refs/
│ ├── heads/
│ │ ├── main ← "main branch 가 가리키는 commit SHA"
│ │ └── feat/x
│ ├── tags/
│ │ └── v1.0
│ └── remotes/origin/...
├── logs/ ← reflog (HEAD 의 모든 이동 기록)
├── hooks/ ← pre-commit 등 script
└── index ← staging area (binary)Git 의 모든 것이 .git/objects/ 안 file 들 + .git/refs/ 안 pointer 들. branch 는 사실 1 줄짜리 text file.
4 가지 object type
1. Blob — 파일 내용
$ echo "hello" | git hash-object --stdin
ce013625030ba8dba906f756967f9e9ca394464a
$ git cat-file -p ce0136...
hello
$ git cat-file -t ce0136...
blobBlob = byte sequence + length. 파일 이름 X. "hello" 라는 내용이 어떤 파일에 있든 같은 SHA-1.
실제 디스크 — .git/objects/ce/013625... (zlib 압축).
2. Tree — 디렉토리 구조
$ git cat-file -p 8a3f...
100644 blob ce0136... README.md
040000 tree 5b2a4f... src
100644 blob 7a8b9c... package.json
↑ 여기 처음 파일 이름이 등장Tree = file mode + object type + SHA + name 의 리스트. blob 의 이름이 tree 에서 결정. tree 가 다른 tree 를 자식으로 가질 수 있어 디렉토리 hierarchy.
3. Commit — 한 시점의 snapshot
$ git cat-file -p HEAD
tree 8a3f... ← snapshot 의 root tree
parent f1a2... ← 이전 commit
author Alice <alice@example.com> 1700000000 +0900
committer Alice <alice@example.com> 1700000000 +0900
feat: add login
This commit adds login functionality.Commit = 한 tree (snapshot) + parent commits + author + message."이 commit 이 변경한 것" 정보 없음 — diff 는 자식 과 부모의 tree 차이를 그때 계산.
Merge commit 은 parent 2 개. Root commit 은 parent 0 개.
4. Tag (annotated)
$ git cat-file -p v1.0
object f1a2... ← 가리키는 commit
type commit
tag v1.0
tagger Alice <alice@example.com> 1700000000 +0900
Release v1.0Annotated tag = commit 의 immutable bookmark + message + tagger. Lightweight tag (git tag v1.0) 는 단순 ref, object 없음.
SHA-1 — content addressable storage
각 object 의 SHA-1 = object 의 내용 + type 의 hash. 같은 내용은 같은 SHA-1. 이게 Git 의 핵심 트릭:
- 중복 자동 제거 — 같은 파일이 100 commit 에 박혀 있어도 blob 1 개만 저장
- 무결성 — object 변경 시 SHA 도 바뀜. 손상 즉시 감지
- 분산 동기 — 두 사람이 같은 commit 만들면 같은 SHA. 같은 history 자연 합쳐짐
SHA-1 충돌 가능성? 2017 SHAttered 가 의도적 충돌 시연. Git 은 2018 부터 SHA-256 migration 진행 중. 일반 사용자는 무관 (의도적 충돌만 위험).
Ref — branch · tag 의 실체
$ cat .git/refs/heads/main
f1a2b3c4d5e6f7...
$ cat .git/HEAD
ref: refs/heads/main
→ HEAD 가 main 을 가리키고, main 이 commit f1a2... 를 가리킴branch 는 commit SHA 1 개를 적은 1 줄짜리 text file. 새 commit 만들면 그 file 의 내용만 바뀜.
Detached HEAD = HEAD 가 branch ref 가 아닌 commit SHA 직접 가리킴 (git checkout f1a2...). 이때 새 commit 만들면 어느 branch 에도 안 박힘 → garbage collection 위험.
변경의 흐름 — staging → commit
Working Tree (file system) — 사용자가 편집하는 파일
│
│ git add
↓
Staging Area (index, .git/index binary)
│
│ git commit
↓
Object Database (.git/objects/)
│
│ git push
↓
Remote repoIndex 는 사실 "다음 commit 이 될 tree" 의 미리보기. git diff = working tree vs index. git diff --cached = index vs HEAD.
Merge vs Rebase — history 모양
Merge — history 그대로
Before:
main: A → B → C
feat: → D → E (from B)
git merge feat:
main: A → B → C → M
↑
merge commit M (parent: C, E)
tree: feat 의 변경 + main 의 변경 합친 결과Rebase — feat 를 main 위로 재작성
Before:
main: A → B → C
feat: → D → E (from B)
git rebase main (from feat):
main: A → B → C
feat: → D' → E' (new commits, different SHA)
D' = D 의 변경을 C 위에 다시 적용. SHA 다름 (parent 다르니).
Git 이 cherry-pick 반복한 것과 동일.Merge vs Rebase 의 핵심 — merge 는 history 보존, rebase 는 rewrite. 공유된 branch (push 한 main) rebase X — 다른 사람의 SHA 깨짐.
Reset — branch ref 이동
Before:
main → C (HEAD)
parent chain: A → B → C
git reset --soft B:
main → B (HEAD)
staging: C 의 변경 그대로
working tree: C 의 변경 그대로
git reset --mixed B (default):
main → B
staging: HEAD 와 같음 (B)
working tree: C 의 변경 그대로
git reset --hard B:
main → B
staging: B
working tree: B (C 의 변경 영구 손실!)Reset 은 단순히 branch ref 가 가리키는 commit 변경. C 의 object 는 그대로 .git/objects/ 에 남음 — git reflog 로 복구 가능 (90 일).
Pack file — 효율 저장
새 object 는 loose (.git/objects/ab/cdef...) 로 저장. 시간 지나면 수천 개 file → file system 부담 + 디스크 낭비 (zlib 만 적용).
git gc 또는 자동 trigger 시:
- 여러 object 를 한 pack file 로 묶음 (
.git/objects/pack/) - delta compression — 비슷한 blob (commit 간 1 줄 변경) 을 delta 로만 저장
- repository 크기 대폭 감소
$ du -sh .git/objects/
초기: 50 MB (loose)
git gc: 5 MB (pack)
↑ 10× 감소 (대형 repo 는 더)Garbage Collection
unreferenced object (어느 ref 에서도 도달 X) 는 GC 대상:
git gc --prune=now→ 즉시 GC- default — 2 주 이상 unreferenced object + reflog 도 만료된 것만
git reset --hard후 commit 은 reflog 에 남음 → 90 일 안 복구 가능
흔한 함정
1. force push 의 위험
A 가 force push main:
local: A → B → D
remote: A → B → C → E (다른 사용자 B 가 push 한 것)
A 의 force push 후:
remote: A → B → D
→ C, E 손실. B 의 작업 사라짐.
해결 — git push --force-with-lease (사전 fetch 한 상태에 한해서만 force)2. detached HEAD 에서 commit 잃기
git checkout abc123 후 commit → 어느 branch 에도 안 박힘. checkout main 하면 commit reflog 외엔 사라진 듯. reflog 에서 SHA 찾아 새 branch 로 복구.
3. .gitignore 무시 안 됨
이미 tracked 된 파일은 .gitignore 추가해도 무시 X. git rm --cached file 로 untrack 후 commit.
4. submodule 의 detached HEAD
submodule 안 commit 하려면 — submodule cd → branch checkout → commit → push. parent repo 에서도 submodule pointer 업데이트 commit.
5. LFS 미사용으로 큰 binary 박힘
대용량 binary (image / video / ML model) commit → repo 크기 폭주. 한 번 박히면 history rewrite 외엔 제거 어려움. Git LFS 또는 gitignore.
참고 자료
- Pro Git book (Scott Chacon) — Internals chapter — git-scm.com
- Git from the inside out (Mary Rose Cook) — maryrosecook.com
- Git for Computer Scientists — eagain.net
- Git SHA-256 transition — git-scm.com
요약
- Git = content addressable file system. .git/objects/ 안 4 object (blob / tree / commit / tag) + .git/refs/ 안 pointer.
- SHA-1 = object 내용의 hash → 중복 자동 제거 + 무결성 + 분산 동기.
- Commit 은 tree (snapshot) + parent + message. diff 는 그때 계산.
- Branch = 1 줄 text file (.git/refs/heads/...). 가벼움.
- Merge = history 보존 + merge commit. Rebase = 새 SHA 로 history rewrite.
- Reset = branch ref 이동. soft/mixed/hard 는 staging/working tree 처리 차이.
- Pack file + delta compression 으로 repo 크기 ↓. git gc 가 자동.
- Reflog (90 일) 가 안전망 — reset/checkout 사고 복구.