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 폴더를 이제 제대로 들여다봅시다.

BASH
git init demo && cd demo
ls .git
CODE
HEAD  config  description  hooks/  info/  objects/  refs/

이 중 핵심은 objects/ 입니다. Git의 모든 데이터(파일 내용, 디렉터리, 커밋)가 여기에 객체로 저장됩니다.

BASH
ls .git/objects
CODE
info  pack         # 아직 아무 객체도 없음 (비어 있음)

14.3 첫 객체 만들기 — git hash-object

git hash-object는 내용을 받아 SHA-1 해시를 계산하고, 객체로 저장하는 저수준(plumbing) 명령입니다.

BASH
# 내용의 해시만 계산 (저장은 안 함)
echo "Hello Git" | git hash-object --stdin
CODE
24b9da6552252987aa493b52f8696cd6d3b00373

-w 옵션을 붙이면 실제로 객체 데이터베이스에 저장합니다.

BASH
echo "Hello Git" | git hash-object -w --stdin
CODE
24b9da6552252987aa493b52f8696cd6d3b00373

이제 objects/ 안을 보면 객체가 생겼습니다.

BASH
find .git/objects -type f
CODE
.git/objects/24/b9da6552252987aa493b52f8696cd6d3b00373
📌 핵심
폴더 구조의 비밀: Git은 40자리 해시의 앞 2자리를 폴더 이름, 나머지 38자리를 파일 이름으로 씁니다. 한 폴더에 객체가 수십만 개 몰리는 걸 막아 성능을 유지하려는 설계입니다. 폴더는 최대 256개(00~ff)까지 만들어집니다.

14.4 객체 들여다보기 — git cat-file

저장한 객체의 내용과 종류를 확인하는 명령입니다.

BASH
# 객체의 타입 보기 (-t = type)
git cat-file -t 24b9da
CODE
blob
BASH
# 객체의 내용 보기 (-p = pretty print)
git cat-file -p 24b9da
CODE
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 — 파일 내용 (이름은 없다!)

⚠️ 흔한 실수
중요: Blob은 파일 이름을 저장하지 않습니다. 오직 내용만 저장합니다. 그래서 내용이 같은 두 파일은 이름이 달라도 같은 blob 하나를 공유합니다.

이것이 6장에서 본 "Git이 같은 내용의 blob을 재사용한다"는 말의 정체입니다. a.txtb.txt의 내용이 같으면, blob은 하나만 저장되고 두 파일이 그것을 가리킵니다. 공간이 절약되죠.

BASH
# 내용이 같으면 해시도 같다 → 같은 blob
echo "same" | git hash-object --stdin    # → 동일 해시
echo "same" | git hash-object --stdin    # → 동일 해시

② Tree — 디렉터리 (이름은 여기 있다)

파일 이름은 blob이 아니라 tree가 가지고 있습니다. tree는 "이 이름의 파일은 이 blob, 저 폴더는 저 tree"라는 매핑 표입니다.

BASH
# 현재 스테이지 상태를 tree 객체로 저장
git add .
git write-tree
CODE
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
BASH
git cat-file -p d8329f
CODE
100644 blob 24b9da...    hello.txt      ← 권한 / 타입 / blob해시 / 이름
040000 tree a1b2c3...    subfolder      ← 하위 폴더는 또 다른 tree

각 줄의 맨 앞 숫자는 파일 권한(mode)입니다.

mode의미
100644일반 파일
100755실행 가능 파일
040000디렉터리(tree)
120000심볼릭 링크
💡 팁
심볼릭 링크(symbolic link, 줄여서 심링크)는 "다른 파일을 가리키는 바로가기"입니다. Windows의 바로가기 아이콘과 비슷하게, 실제 내용 대신 다른 경로를 가리킵니다.

③ Commit — 스냅샷 + 메타데이터

커밋 객체는 하나의 tree(최상위 디렉터리 스냅샷)를 가리키고, 거기에 작성자·시간·메시지·부모 커밋을 더합니다.

BASH
git log
git cat-file -p HEAD
CODE
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, 즉 현재 브랜치를 가리키는 텍스트 한 줄
BASH
cat .git/HEAD
CODE
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 객체 생성

스테이지에 무엇이 들어있는지 직접 볼 수 있습니다.

BASH
git ls-files -s      # 스테이지(index)의 파일 목록 + blob 해시 + 권한

14.7 SHA-1 해시란 무엇인가?

Git이 객체의 키로 쓰는 SHA-1해시 함수입니다. 해시 함수는 임의 길이의 입력을 받아 고정 길이(SHA-1은 160비트 = 40자리 16진수)의 지문을 출력합니다.

해시 함수의 핵심 성질:

  1. 같은 입력 → 항상 같은 출력 (그래서 내용이 같으면 해시도 같음)
  2. 입력이 조금만 달라도 → 출력이 완전히 달라짐
  3. 출력으로 입력을 되돌릴 수 없음 (단방향)
BASH
echo "Hello Git"  | git hash-object --stdin   # 24b9da65...
echo "Hello Git!" | git hash-object --stdin   # 전혀 다른 해시 (느낌표 하나 차이)
📌 핵심
Git이 내용을 그대로 해시하는 건 아니고, "blob <크기>\0<내용>" 형태의 헤더를 붙인 뒤 해시합니다. 그래서 hash-object의 결과가 단순 sha1sum과 다릅니다. 깊이 알 필요는 없지만, "타입과 크기까지 포함해 해시한다"는 점만 기억하세요.

14.8 해시 충돌은 걱정 안 해도 될까?

"내용이 다른데 우연히 같은 해시가 나오면(충돌) 어쩌나?" — 이론적으로 가능하지만, 현실적으로 일어나지 않습니다.

SHA-1은 2^160개의 서로 다른 값을 가질 수 있습니다. 이 수는 약 1.46 × 10^48로, 상상하기 어려운 크기입니다.

🎲 직관적 비유: 지구상 모든 사람이 매초 수십억 개의 객체를 만들어도, 우주의 나이만큼 시간이 지나도 충돌을 보기 어렵습니다. 실무에서 두 개의 의미 있는 파일이 우연히 같은 SHA-1을 갖는 일은 사실상 0입니다.
⚠️ 흔한 실수
보안 관점: SHA-1은 의도적 공격(collision attack)에는 2017년에 깨진 전례가 있어, Git은 이미 충돌 탐지 코드를 넣어두었습니다. 그리고 5장·1장에서 언급했듯 Git 3.0부터는 더 안전한 SHA-256이 기본이 됩니다. 일상 무결성에는 SHA-1도 충분하지만, 미래는 SHA-256입니다.

14.9 저수준 명령으로 커밋 한 번 만들어보기 (선택)

평소 쓰는 add/commit이 내부적으로 무엇을 하는지, 저수준 명령으로 분해해보면 명확해집니다.

BASH
# 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 과정을 자동화한 고수준 명령

✍️ 확인 문제

  1. 파일 이름을 저장하는 객체는 blob인가요 tree인가요?
  2. 내용이 완전히 같은 두 파일을 add하면 blob이 몇 개 생기나요?
  3. git cat-file -p HEAD를 하면 무엇을 볼 수 있나요?
다음 장에서는 터미널 작업의 기본기인 셸 명령어를 정리합니다. → 15_셸명령어기초.md