14. Git 내부 동작 원리 — 후드 아래 들여다보기
- Git이 데이터를 저장하는 3대 객체(blob, tree, commit)를 이해한다
- SHA-1 해시가 무엇이고 왜 충돌 걱정이 없는지 안다
hash-object,cat-file로 Git 데이터베이스를 직접 들여다본다- "Git은 사실 작은 데이터베이스"라는 본질을 깨닫는다
14.1 Git은 "콘텐츠 주소 지정 파일 시스템"
대부분의 사람은 Git을 "버전 관리 명령어 모음"으로 생각합니다. 하지만 그 본질은 키-값 데이터베이스(key-value store)입니다.
- 값(value): 어떤 내용(파일 내용, 디렉터리 구조, 커밋 정보)
- 키(key): 그 내용을 SHA-1 해시로 계산한 40자리 지문
즉, "내용을 넣으면 지문(키)을 돌려주고, 그 지문으로 내용을 다시 꺼낼 수 있는" 저장소입니다. 이것을 "콘텐츠 주소 지정(content-addressable)"이라 부릅니다. 파일 이름이 아니라 내용 자체가 주소가 되는 것이죠.
14.2 .git 폴더 다시 보기
3장에서 잠깐 봤던 .git 폴더를 이제 제대로 들여다봅시다.
git init demo && cd demo ls .git
HEAD config description hooks/ info/ objects/ refs/
이 중 핵심은 objects/ 입니다. Git의 모든 데이터(파일 내용, 디렉터리, 커밋)가 여기에 객체로 저장됩니다.
ls .git/objects
info pack # 아직 아무 객체도 없음 (비어 있음)
14.3 첫 객체 만들기 — git hash-object
git hash-object는 내용을 받아 SHA-1 해시를 계산하고, 객체로 저장하는 저수준(plumbing) 명령입니다.
# 내용의 해시만 계산 (저장은 안 함) echo "Hello Git" | git hash-object --stdin
24b9da6552252987aa493b52f8696cd6d3b00373
-w 옵션을 붙이면 실제로 객체 데이터베이스에 저장합니다.
echo "Hello Git" | git hash-object -w --stdin
24b9da6552252987aa493b52f8696cd6d3b00373
이제 objects/ 안을 보면 객체가 생겼습니다.
find .git/objects -type f
.git/objects/24/b9da6552252987aa493b52f8696cd6d3b00373
00~ff)까지 만들어집니다.14.4 객체 들여다보기 — git cat-file
저장한 객체의 내용과 종류를 확인하는 명령입니다.
# 객체의 타입 보기 (-t = type) git cat-file -t 24b9da
blob
# 객체의 내용 보기 (-p = pretty print) git cat-file -p 24b9da
Hello Git
해시 앞 6자리만 줘도 Git이 알아서 찾아줍니다. 방금 저장한 내용이 그대로 나옵니다.
14.5 Git의 3대 객체 타입
Git 데이터베이스에는 사실상 세 종류의 객체만 있습니다. 이 셋의 관계를 이해하면 Git의 전부를 이해한 것입니다.
flowchart LR
Commit["📸 Commit<br/>커밋<br/><small>누가/언제/메시지</small>"]
Tree["📁 Tree<br/>디렉터리<br/><small>이름↔해시 매핑</small>"]
Blob["📄 Blob<br/>파일 내용<br/><small>실제 데이터</small>"]
Commit -- "가리킴" --> Tree
Tree -- "가리킴" --> Blob
classDef commit fill:#f3e8ff,stroke:#9333ea,color:#6b21a8
classDef tree fill:#dbeafe,stroke:#2563eb,color:#1e40af
classDef blob fill:#dcfce7,stroke:#16a34a,color:#166534
class Commit commit
class Tree tree
class Blob blob
| 객체 | 역할 | 비유 |
|---|---|---|
| Blob | 파일의 내용을 저장 | 파일 (이름 없는 순수 내용) |
| Tree | 디렉터리 구조 (파일명↔blob 매핑) | 폴더 |
| Commit | 특정 시점의 스냅샷 + 메타데이터 | 사진 + 라벨(작성자/시간/메시지) |
① Blob — 파일 내용 (이름은 없다!)
이것이 6장에서 본 "Git이 같은 내용의 blob을 재사용한다"는 말의 정체입니다. a.txt와 b.txt의 내용이 같으면, blob은 하나만 저장되고 두 파일이 그것을 가리킵니다. 공간이 절약되죠.
# 내용이 같으면 해시도 같다 → 같은 blob echo "same" | git hash-object --stdin # → 동일 해시 echo "same" | git hash-object --stdin # → 동일 해시
② Tree — 디렉터리 (이름은 여기 있다)
파일 이름은 blob이 아니라 tree가 가지고 있습니다. tree는 "이 이름의 파일은 이 blob, 저 폴더는 저 tree"라는 매핑 표입니다.
# 현재 스테이지 상태를 tree 객체로 저장 git add . git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
git cat-file -p d8329f
100644 blob 24b9da... hello.txt ← 권한 / 타입 / blob해시 / 이름 040000 tree a1b2c3... subfolder ← 하위 폴더는 또 다른 tree
각 줄의 맨 앞 숫자는 파일 권한(mode)입니다.
| mode | 의미 |
|---|---|
100644 | 일반 파일 |
100755 | 실행 가능 파일 |
040000 | 디렉터리(tree) |
120000 | 심볼릭 링크 |
③ Commit — 스냅샷 + 메타데이터
커밋 객체는 하나의 tree(최상위 디렉터리 스냅샷)를 가리키고, 거기에 작성자·시간·메시지·부모 커밋을 더합니다.
git log git cat-file -p HEAD
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 ← 이 커밋의 디렉터리 스냅샷
parent 9f8e7d6... ← 이전 커밋 (첫 커밋은 없음)
author Hong Gildong <hong@example.com> 1718... ← 작성자 + 타임스탬프
committer Hong Gildong <hong@example.com> 1718...
← 빈 줄 뒤
Add hello file ← 커밋 메시지
- 커밋 해시가 매번 다른 이유 → 부모·시간·작성자가 모두 해시 계산에 들어가니까
- 브랜치가 가벼운 이유 → 그냥 커밋 해시를 담은 41바이트 파일이니까
- HEAD의 정체 →
.git/HEAD를 열면ref: refs/heads/main, 즉 현재 브랜치를 가리키는 텍스트 한 줄
cat .git/HEAD
ref: refs/heads/main
14.6 워킹 디렉터리 / 스테이지 / 저장소를 객체로 다시 보기
3장의 세 영역을, 이제 객체 관점에서 다시 봅시다.
flowchart LR
WD["🖥️ 워킹 디렉터리<br/><small>실제 파일들</small>"]
SA["📦 스테이지 (index)<br/><small>tree 형태로 기록</small>"]
REPO["🗄️ 저장소 (objects)<br/><small>commit 객체 생성</small>"]
WD -- "git add<br/>(blob 저장)" --> SA
SA -- "git commit<br/>(tree+메타데이터)" --> REPO
classDef work fill:#fef3c7,stroke:#d97706,color:#92400e
classDef stage fill:#dbeafe,stroke:#2563eb,color:#1e40af
classDef repo fill:#dcfce7,stroke:#16a34a,color:#166534
class WD work
class SA stage
class REPO repo
git add→ 파일 내용이 blob으로 저장되고, 스테이지(.git/index)에 등록git commit→ 스테이지 상태가 tree로 굳고, 이를 가리키는 commit 객체 생성
스테이지에 무엇이 들어있는지 직접 볼 수 있습니다.
git ls-files -s # 스테이지(index)의 파일 목록 + blob 해시 + 권한
14.7 SHA-1 해시란 무엇인가?
Git이 객체의 키로 쓰는 SHA-1은 해시 함수입니다. 해시 함수는 임의 길이의 입력을 받아 고정 길이(SHA-1은 160비트 = 40자리 16진수)의 지문을 출력합니다.
해시 함수의 핵심 성질:
- 같은 입력 → 항상 같은 출력 (그래서 내용이 같으면 해시도 같음)
- 입력이 조금만 달라도 → 출력이 완전히 달라짐
- 출력으로 입력을 되돌릴 수 없음 (단방향)
echo "Hello Git" | git hash-object --stdin # 24b9da65... echo "Hello Git!" | git hash-object --stdin # 전혀 다른 해시 (느낌표 하나 차이)
"blob <크기>\0<내용>" 형태의 헤더를 붙인 뒤 해시합니다. 그래서 hash-object의 결과가 단순 sha1sum과 다릅니다. 깊이 알 필요는 없지만, "타입과 크기까지 포함해 해시한다"는 점만 기억하세요.14.8 해시 충돌은 걱정 안 해도 될까?
"내용이 다른데 우연히 같은 해시가 나오면(충돌) 어쩌나?" — 이론적으로 가능하지만, 현실적으로 일어나지 않습니다.
SHA-1은 2^160개의 서로 다른 값을 가질 수 있습니다. 이 수는 약 1.46 × 10^48로, 상상하기 어려운 크기입니다.
🎲 직관적 비유: 지구상 모든 사람이 매초 수십억 개의 객체를 만들어도, 우주의 나이만큼 시간이 지나도 충돌을 보기 어렵습니다. 실무에서 두 개의 의미 있는 파일이 우연히 같은 SHA-1을 갖는 일은 사실상 0입니다.
14.9 저수준 명령으로 커밋 한 번 만들어보기 (선택)
평소 쓰는 add/commit이 내부적으로 무엇을 하는지, 저수준 명령으로 분해해보면 명확해집니다.
# 1. 파일 내용을 blob으로 저장 echo "content" | git hash-object -w --stdin # → blob 해시 # 2. 스테이지에 등록 (read-tree / update-index 계열) git add file.txt # 3. 스테이지를 tree로 굳히기 git write-tree # → tree 해시 # 4. tree를 가리키는 commit 객체 만들기 echo "my message" | git commit-tree <tree해시> # → commit 해시
git commit 한 줄이 사실은 이 모든 과정을 자동으로 해주는 고수준(porcelain) 명령이었던 것입니다.
14.10 이 장에서 배운 것 (요약)
- Git의 본질은 콘텐츠 주소 지정 키-값 데이터베이스 (내용→해시→내용)
- 3대 객체: blob(파일 내용, 이름 없음) / tree(디렉터리, 이름 보유) / commit(스냅샷+메타데이터)
git hash-object -w(저장),git cat-file -t/-p(타입/내용 확인)- 객체는
.git/objects/에 앞2자리 폴더 + 뒤38자리 파일로 저장 - 같은 내용의 파일은 같은 blob을 재사용(공간 절약)
- SHA-1 해시: 같은 입력=같은 출력, 단방향, 충돌은 사실상 불가 (미래는 SHA-256)
git commit은 blob→tree→commit 과정을 자동화한 고수준 명령
✍️ 확인 문제
- 파일 이름을 저장하는 객체는 blob인가요 tree인가요?
- 내용이 완전히 같은 두 파일을 add하면 blob이 몇 개 생기나요?
git cat-file -p HEAD를 하면 무엇을 볼 수 있나요?
다음 장에서는 터미널 작업의 기본기인 셸 명령어를 정리합니다. → 15_셸명령어기초.md