Skip to content
Published on

Git 내부 동작 원리: Blob, Tree, Commit, 그리고 DAG — Git이 진짜로 작동하는 방법

Authors

들어가며: 왜 Git 내부를 이해해야 하는가

대부분의 개발자는 git add, git commit, git push 정도만 사용합니다. 하지만 Git 내부 동작을 이해하면 다음과 같은 이점이 있습니다.

  • 디버깅 능력 향상: detached HEAD, 충돌, 잃어버린 커밋을 정확히 이해하고 복구할 수 있음
  • 고급 워크플로우 활용: rebase, cherry-pick, bisect를 자신 있게 사용
  • 기술 면접 대비: FAANG급 회사에서 Git 내부 구조 질문이 빈번
  • 트러블슈팅: 저장소 손상, 대용량 파일 문제, 느린 clone 해결

이 글에서는 Git의 내부를 바닥부터 해부합니다. 객체 모델, 해싱, 디렉토리 구조, DAG, 브랜치의 실체, merge/rebase 내부 동작, reflog, pack 파일까지 모든 것을 다룹니다.


1. Git 객체 모델 — 모든 것의 기초

Git은 본질적으로 Content-Addressable Storage(내용 주소 지정 저장소)입니다. 파일 내용의 SHA-1 해시를 키로 사용하여 4가지 타입의 객체를 저장합니다.

1.1 Blob (Binary Large Object)

Blob은 파일의 내용만 저장합니다. 파일명이나 경로 정보는 포함하지 않습니다.

# 파일 내용으로부터 blob 생성
echo "Hello, Git!" | git hash-object --stdin
# 출력: 0907f4a3c4740fa3a5c919cb4447fdb1f1a66aec

# blob 내용 확인
git cat-file -p 0907f4a
# 출력: Hello, Git!

# blob 타입 확인
git cat-file -t 0907f4a
# 출력: blob

핵심 포인트: 동일한 내용의 파일이 100개 있어도 blob은 딱 1개만 저장됩니다. 이것이 Git이 효율적인 이유입니다.

1.2 Tree (트리)

Tree는 디렉토리 구조를 나타냅니다. 파일명, 파일 모드, blob/tree 참조를 포함합니다.

# 최신 커밋의 tree 확인
git cat-file -p HEAD^{tree}
# 출력:
# 100644 blob a1b2c3d... README.md
# 100644 blob d4e5f6a... package.json
# 040000 tree b7c8d9e... src
Tree (root)
├── blob: README.md (100644)
├── blob: package.json (100644)
└── tree: src/
    ├── blob: index.ts (100644)
    └── blob: utils.ts (100644)

파일 모드 의미:

모드의미
100644일반 파일
100755실행 파일
120000심볼릭 링크
040000디렉토리 (tree)

1.3 Commit (커밋)

Commit 객체는 스냅샷의 메타데이터를 저장합니다.

git cat-file -p HEAD
# 출력:
# tree a1b2c3d4e5f6...
# parent 9f8e7d6c5b4a...
# author Kim <kim@example.com> 1711234567 +0900
# committer Kim <kim@example.com> 1711234567 +0900
#
# feat: add user authentication

Commit 객체의 구성 요소:

  • tree: 이 커밋 시점의 전체 프로젝트 스냅샷 (root tree 참조)
  • parent: 부모 커밋 SHA-1 (최초 커밋은 parent 없음, merge 커밋은 2개 이상)
  • author: 코드를 작성한 사람
  • committer: 커밋을 만든 사람 (cherry-pick 시 다를 수 있음)
  • message: 커밋 메시지

1.4 Tag (태그)

Annotated tag는 별도의 객체로 저장됩니다.

# annotated tag 생성
git tag -a v1.0.0 -m "Release version 1.0.0"

# tag 객체 확인
git cat-file -p v1.0.0
# 출력:
# object d4e5f6a7b8c9...
# type commit
# tag v1.0.0
# tagger Kim <kim@example.com> 1711234567 +0900
#
# Release version 1.0.0

1.5 객체 관계도

Tag ──▶ Commit ──▶ Tree ──▶ Blob
           │         │
           ▼         ▼
        Commit     Tree ──▶ Blob
        (parent)

2. SHA-1 해싱과 Content-Addressable Storage

2.1 SHA-1이 작동하는 방식

Git은 객체의 내용에 헤더를 붙여 SHA-1 해시를 계산합니다.

# Git이 내부적으로 하는 계산
content="Hello, Git!"
header="blob ${#content}\0"
echo -en "${header}${content}" | sha1sum
# 결과: 0907f4a3c4740fa3a5c919cb4447fdb1f1a66aec

헤더 형식: 타입 크기\0내용

blob 11\0Hello, Git!
└─┬─┘└┬┘└┬┘└────┬────┘
 타입 크기 널  실제 내용

2.2 Content-Addressable의 의미

같은 내용은 항상 같은 해시를 생성합니다. 이것은 다음을 보장합니다.

  • 무결성: 데이터가 변조되면 해시가 달라짐
  • 중복 제거: 같은 파일은 한 번만 저장
  • 효율적 비교: 해시만 비교하면 내용이 같은지 즉시 판단

2.3 SHA-1 충돌과 SHA-256 전환

2017년 Google이 SHA-1 충돌을 실증했습니다(SHAttered 공격). Git은 이에 대응하여 SHA-256으로의 전환을 진행 중입니다.

# SHA-256 저장소 생성 (Git 2.29+)
git init --object-format=sha256

# 현재 저장소의 해시 알고리즘 확인
git rev-parse --show-object-format
항목SHA-1SHA-256
해시 길이40자64자
보안성충돌 발견됨안전
호환성모든 Git 버전Git 2.29+
상태기본값실험적

3. .git 디렉토리 해부

git init 후 생성되는 .git 디렉토리의 구조를 살펴봅시다.

.git/
├── HEAD              # 현재 체크아웃된 브랜치 참조
├── config            # 저장소별 설정
├── description       # GitWeb용 설명 (거의 안 씀)
├── hooks/            # 클라이언트/서버 훅 스크립트
│   ├── pre-commit.sample
│   ├── commit-msg.sample
│   └── ...
├── info/
│   └── exclude       # .gitignore의 로컬 버전
├── objects/          # 모든 Git 객체 저장소
│   ├── 09/
│   │   └── 07f4a3c4740fa3a5c919cb4447fdb1f1a66aec
│   ├── info/
│   └── pack/         # pack 파일들
├── refs/             # 브랜치와 태그 참조
│   ├── heads/        # 로컬 브랜치
│   │   └── main
│   ├── remotes/      # 원격 브랜치
│   │   └── origin/
│   └── tags/         # 태그
└── index             # 스테이징 영역 (바이너리)

3.1 HEAD 파일

# HEAD는 현재 브랜치를 가리킴
cat .git/HEAD
# ref: refs/heads/main

# detached HEAD 상태에서는 커밋 해시를 직접 가리킴
git checkout a1b2c3d
cat .git/HEAD
# a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0

3.2 refs 디렉토리

# 브랜치는 단순히 커밋 해시를 담은 파일
cat .git/refs/heads/main
# d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3

# 수동으로 브랜치 생성도 가능!
echo "d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3" > .git/refs/heads/my-branch

3.3 objects 디렉토리

객체는 SHA-1 해시의 처음 2자로 디렉토리를 만들고, 나머지 38자로 파일명을 지정합니다.

# 해시: 0907f4a3c4740fa3a5c919cb4447fdb1f1a66aec
# 저장 위치: .git/objects/09/07f4a3c4740fa3a5c919cb4447fdb1f1a66aec

# 객체는 zlib으로 압축됨
python3 -c "
import zlib
with open('.git/objects/09/07f4a3c4740fa3a5c919cb4447fdb1f1a66aec', 'rb') as f:
    print(zlib.decompress(f.read()))
"

3.4 index 파일 (Staging Area)

# 스테이징 영역 내용 확인
git ls-files --stage
# 100644 a1b2c3d4... 0    README.md
# 100644 d4e5f6a7... 0    src/index.ts

# index는 바이너리 파일
# "DIRC" 시그니처로 시작
hexdump -C .git/index | head -3

4. DAG와 커밋 그래프

4.1 DAG(Directed Acyclic Graph)란

Git의 커밋 히스토리는 방향 비순환 그래프(DAG) 자료구조입니다.

  • 방향(Directed): 커밋이 부모를 가리킴 (자식에서 부모 방향)
  • 비순환(Acyclic): 순환 참조 불가능 (A가 B의 부모이면서 동시에 자식일 수 없음)
  • 그래프(Graph): 노드(커밋)와 엣지(부모 참조)로 구성
    A ◀── B ◀── C ◀── D  (main)
                 └── E ◀── F  (feature)

4.2 커밋 그래프 시각화

# 그래프 시각화
git log --oneline --graph --all

# 예시 출력:
# *   f1a2b3c (HEAD -> main) Merge branch 'feature'
# |\
# | * d4e5f6a (feature) feat: add search
# | * a7b8c9d feat: add filter
# |/
# * 1e2f3a4 initial commit

4.3 부모 포인터

# 첫 번째 부모 (merge 커밋에서)
git log --first-parent --oneline

# 부모 커밋 접근
git cat-file -p HEAD^1   # 첫 번째 부모
git cat-file -p HEAD^2   # 두 번째 부모 (merge 커밋)
git cat-file -p HEAD~3   # 3단계 위 조상

부모 참조 문법:

HEAD~1 = HEAD^  = 첫 번째 부모
HEAD~2 = HEAD^^ = 첫 번째 부모의 첫 번째 부모
HEAD^2           = 두 번째 부모 (merge 커밋에서만)

5. 브랜치와 태그 — 단순한 포인터

5.1 브랜치는 포인터다

Git 브랜치는 특정 커밋을 가리키는 40바이트 파일에 불과합니다.

# 브랜치 생성 = 파일 생성
git branch feature
# 위 명령은 사실상 아래와 같음:
# echo $(git rev-parse HEAD) > .git/refs/heads/feature

# 브랜치 전환 = HEAD 파일 수정
git checkout feature
# 위 명령은 사실상:
# 1. .git/HEAD에 "ref: refs/heads/feature" 기록
# 2. 워킹 트리를 해당 커밋의 tree로 업데이트
# 3. index를 해당 tree에 맞게 업데이트
.git/refs/heads/
├── main     → d4e5f6a (commit)
├── feature  → a7b8c9d (commit)
└── hotfix   → 1e2f3a4 (commit)

.git/HEAD → ref: refs/heads/main

5.2 HEAD의 역할

HEAD는 현재 체크아웃된 위치를 가리키는 포인터입니다.

정상 상태:
HEAD → refs/heads/main → commit d4e5f6a

Detached HEAD 상태:
HEAD → commit a7b8c9d (직접 커밋을 가리킴)

Detached HEAD 주의점: 이 상태에서 커밋하면 브랜치가 없어서 나중에 참조할 수 없습니다. git gc가 실행되면 해당 커밋이 삭제될 수 있습니다.

# detached HEAD 상태에서 작업 저장
git checkout -b recovery-branch

5.3 Lightweight vs Annotated Tag

# Lightweight tag: 단순 참조 (refs/tags/에 커밋 해시만 저장)
git tag v1.0.0-rc1

# Annotated tag: 별도 tag 객체 생성
git tag -a v1.0.0 -m "Release 1.0.0"

# 차이점 확인
git cat-file -t v1.0.0-rc1  # commit
git cat-file -t v1.0.0      # tag

6. Merge 내부 동작

6.1 Fast-Forward Merge

main에서 feature가 분기한 후, main에 추가 커밋이 없는 경우:

Before:
A ◀── B ◀── C (main)
              ◀── D ◀── E (feature)

After (fast-forward):
A ◀── B ◀── C ◀── D ◀── E (main, feature)
# fast-forward merge
git checkout main
git merge feature
# "Fast-forward" 메시지 출력

# fast-forward를 하지 않고 merge 커밋 생성 강제
git merge --no-ff feature

Fast-forward는 포인터만 이동하므로 새 커밋이 생성되지 않습니다.

6.2 Three-Way Merge (3방향 병합)

두 브랜치 모두에 새 커밋이 있는 경우:

Before:
A ◀── B ◀── C ◀── F (main)
       ◀── D ◀── E (feature)

After (3-way merge):
A ◀── B ◀── C ◀── F ◀── G (main) [merge commit]
       ◀── D ◀── E ──────┘
              (feature)

3방향 병합 알고리즘:

  1. 공통 조상(Merge Base) 찾기: B가 공통 조상
  2. 두 브랜치의 변경사항 계산: B에서 F까지, B에서 E까지
  3. 변경사항 합치기: 충돌이 없으면 자동 병합, 있으면 수동 해결
# merge base 확인
git merge-base main feature
# 출력: B의 SHA-1 해시

# merge를 위한 3개의 tree 비교
git diff $(git merge-base main feature) main    # base vs main
git diff $(git merge-base main feature) feature  # base vs feature

6.3 Recursive Strategy

Merge base가 여러 개인 경우 Git은 recursive strategy를 사용합니다.

    A ◀── B ◀── E (main)
    ▲      ◀── F (feature)
    └── C ◀── D
    (크로스 merge 히스토리)

이 경우 Git은:

  1. 여러 merge base를 찾음
  2. merge base끼리 먼저 가상 merge
  3. 그 결과를 실제 merge base로 사용
# merge strategy 확인
git merge -s recursive feature
git merge -s ort feature        # Git 2.34+ 기본값 (Ostensibly Recursive's Twin)

6.4 Octopus Merge

3개 이상의 브랜치를 동시에 병합할 때 사용합니다.

git merge feature1 feature2 feature3
    A ◀── B (main)
    ◀── C (feature1)
    ◀── D (feature2)
    ◀── E (feature3)


    A ◀── B ◀── M (main) [3개의 부모]
    ◀── C ──────┘
    ◀── D ──────┘
    ◀── E ──────┘

주의: 충돌이 발생하면 octopus merge는 실패합니다.


7. Rebase 내부 동작

7.1 Rebase의 본질: 커밋 재생성

Rebase는 기존 커밋을 복사하여 새로운 부모 위에 재생성합니다.

Before:
A ◀── B ◀── C (main)
       ◀── D ◀── E (feature)

After rebase (git checkout feature && git rebase main):
A ◀── B ◀── C (main)
              ◀── D' ◀── E' (feature)

중요: D'과 E'는 D, E와 다른 커밋입니다. 내용은 같지만 부모가 다르므로 SHA-1이 다릅니다.

7.2 Rebase 내부 단계

git checkout feature
git rebase main

Git이 내부적으로 수행하는 작업:

  1. feature 브랜치의 커밋 중 main에 없는 것 찾기 (D, E)
  2. 이 커밋들의 패치를 임시 저장 (.git/rebase-apply/ 또는 .git/rebase-merge/)
  3. feature를 main의 최신 커밋(C)으로 reset
  4. 저장한 패치를 하나씩 적용하여 새 커밋 생성 (D', E')
# rebase 중 임시 파일 확인
ls .git/rebase-merge/
# done        # 완료된 커밋
# git-rebase-todo  # 남은 커밋
# head-name   # 원래 브랜치명
# onto        # rebase 대상 커밋

7.3 Interactive Rebase

git rebase -i HEAD~3
# .git/rebase-merge/git-rebase-todo 내용:
pick a1b2c3d feat: add login
pick d4e5f6a feat: add signup
pick 7b8c9d0 fix: typo in login

# 명령어:
# pick   = 커밋 사용
# reword = 메시지 수정
# edit   = 커밋 수정 후 계속
# squash = 이전 커밋과 합치기 (메시지 합침)
# fixup  = 이전 커밋과 합치기 (메시지 버림)
# drop   = 커밋 삭제

7.4 Rebase vs Merge 비교

항목MergeRebase
히스토리분기 유지 (비선형)직선형
기존 커밋변경 없음새 커밋 생성
충돌 해결1번만커밋마다 가능
공유 브랜치안전위험 (force push 필요)
merge 커밋생성됨없음

황금 규칙: 이미 push한 커밋은 rebase하지 마세요. 다른 사람이 그 커밋을 base로 작업했을 수 있습니다.


8. Cherry-Pick과 Revert

8.1 Cherry-Pick 내부 동작

Cherry-pick은 특정 커밋의 변경사항만 현재 브랜치에 적용합니다.

git cherry-pick d4e5f6a

내부 동작:

  1. 대상 커밋(d4e5f6a)과 그 부모 간의 diff 계산
  2. 현재 HEAD에 해당 diff 적용
  3. 새 커밋 생성 (같은 메시지, 다른 SHA-1)
Before:
A ◀── B ◀── C (main)
       ◀── D ◀── E (feature)

git checkout main && git cherry-pick E:
A ◀── B ◀── C ◀── E' (main) [E의 변경사항만 복사]
       ◀── D ◀── E (feature)
# cherry-pick한 커밋의 원본 확인
git log --oneline E'
# author와 committer가 다를 수 있음

8.2 Revert 내부 동작

Revert는 특정 커밋의 역방향 패치를 적용하는 새 커밋을 생성합니다.

git revert d4e5f6a
Before:
A ◀── B ◀── C (main)

git revert B:
A ◀── B ◀── C ◀── B' (main) [B의 변경을 되돌리는 새 커밋]

Revert는 히스토리를 다시 쓰지 않으므로 공유 브랜치에서 안전합니다.

8.3 Merge 커밋 Revert

# merge 커밋을 revert할 때는 어떤 부모를 기준으로 할지 지정
git revert -m 1 MERGE_COMMIT_HASH
# -m 1: 첫 번째 부모(main) 기준으로 되돌림
# -m 2: 두 번째 부모(feature) 기준으로 되돌림

9. Reflog — 안전망

9.1 Reflog란

Reflog는 HEAD와 브랜치 참조의 모든 변경 이력을 기록합니다. 실수로 커밋을 삭제하거나, rebase로 히스토리를 망쳤을 때 복구의 핵심 도구입니다.

# reflog 확인
git reflog
# d4e5f6a HEAD@{0}: commit: feat: add auth
# a1b2c3d HEAD@{1}: checkout: moving from feature to main
# 7b8c9d0 HEAD@{2}: commit: feat: add search
# 1e2f3a4 HEAD@{3}: rebase finished
# ...

# 특정 브랜치의 reflog
git reflog show feature

9.2 Reflog를 이용한 복구 시나리오

시나리오 1: 실수로 hard reset한 경우

# 실수!
git reset --hard HEAD~3

# reflog에서 이전 상태 확인
git reflog
# a1b2c3d HEAD@{1}: 이전 상태

# 복구
git reset --hard a1b2c3d

시나리오 2: rebase 후 원래 상태로 되돌리기

# rebase 전 상태는 reflog에 남아있음
git reflog
# ... HEAD@{5}: rebase (start): checkout main

# rebase 전으로 복구
git reset --hard HEAD@{5}

시나리오 3: 삭제한 브랜치 복구

# 브랜치 삭제
git branch -D feature

# reflog에서 해당 브랜치의 마지막 커밋 찾기
git reflog | grep feature

# 브랜치 재생성
git branch feature a1b2c3d

9.3 Reflog의 만료

# 기본 만료 기간
# 도달 가능한 항목: 90일
# 도달 불가능한 항목: 30일

# 만료 기간 설정
git config gc.reflogExpire "180 days"
git config gc.reflogExpireUnreachable "60 days"

# 수동 만료
git reflog expire --expire=now --all

10. Pack 파일과 가비지 컬렉션

10.1 Loose Objects vs Packed Objects

Git은 처음에 각 객체를 개별 파일(loose object)로 저장합니다. 객체가 많아지면 pack 파일로 압축합니다.

# loose objects 수 확인
find .git/objects -type f | grep -v 'pack\|info' | wc -l

# pack 파일 확인
ls .git/objects/pack/
# pack-a1b2c3d4e5f6.idx   (인덱스)
# pack-a1b2c3d4e5f6.pack  (데이터)

10.2 Delta Compression

Pack 파일은 delta compression을 사용합니다. 비슷한 파일은 차이점만 저장합니다.

# pack 파일 내용 확인
git verify-pack -v .git/objects/pack/pack-*.idx
# SHA-1 type size size-in-pack offset depth base-SHA-1
# a1b2c3d blob 10240 3521 12 0
# d4e5f6a blob 10245 45   1200 1 a1b2c3d  # delta!

위 예시에서 d4e5f6a는 a1b2c3d와의 차이만 45바이트로 저장됩니다.

10.3 git gc (Garbage Collection)

# 가비지 컬렉션 실행
git gc

# 수행하는 작업:
# 1. loose objects를 pack 파일로 압축
# 2. 도달 불가능한 객체 제거
# 3. reflog 정리
# 4. refs를 packed-refs로 압축

# 더 공격적인 GC
git gc --aggressive --prune=now

# GC 통계 확인
git count-objects -v
# count: 0        (loose objects)
# size: 0         (loose objects 크기, KB)
# in-pack: 1234   (pack된 objects)
# packs: 1        (pack 파일 수)
# size-pack: 5678 (pack 파일 크기, KB)
# prune-packable: 0
# garbage: 0

10.4 대용량 파일 문제와 해결

# 저장소에서 가장 큰 파일 찾기
git rev-list --objects --all | \
  git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | \
  sed -n 's/^blob //p' | sort -rnk2 | head -10

# BFG Repo-Cleaner로 대용량 파일 제거
bfg --strip-blobs-bigger-than 100M

# git filter-repo (권장)
git filter-repo --strip-blobs-bigger-than 100M

11. Plumbing vs Porcelain 명령어

Git 명령어는 두 계층으로 나뉩니다.

11.1 Porcelain (도자기) — 사용자 친화적

일상적으로 사용하는 고수준 명령어:

git add, git commit, git push, git pull
git branch, git checkout, git merge, git rebase
git log, git diff, git status
git stash, git tag, git remote

11.2 Plumbing (배관) — 저수준

Git 내부에서 사용하는 저수준 명령어:

# 객체 조작
git hash-object    # 객체 해시 계산 및 저장
git cat-file       # 객체 내용/타입/크기 확인
git write-tree     # index에서 tree 객체 생성
git commit-tree    # tree에서 commit 객체 생성
git update-ref     # 참조 업데이트

# Index 조작
git update-index   # index에 파일 추가
git ls-files       # index 내용 확인
git read-tree      # tree를 index로 읽기

# 전송
git pack-objects   # pack 파일 생성
git unpack-objects # pack 파일 해제
git send-pack      # 객체 전송
git receive-pack   # 객체 수신

11.3 Plumbing으로 커밋 만들기

Porcelain 없이 커밋을 만드는 과정:

# 1. blob 생성
echo "Hello World" | git hash-object -w --stdin
# a1b2c3d...

# 2. index에 추가
git update-index --add --cacheinfo 100644 a1b2c3d hello.txt

# 3. tree 생성
git write-tree
# d4e5f6a...

# 4. commit 생성
echo "first commit" | git commit-tree d4e5f6a
# 7b8c9d0...

# 5. 브랜치가 이 커밋을 가리키게 함
git update-ref refs/heads/main 7b8c9d0

12. 면접 질문 모음 (10문제)

Q1. Git에서 git add를 하면 내부적으로 무슨 일이 일어나는가?

모범 답변: 파일 내용이 SHA-1로 해시되어 blob 객체가 .git/objects/에 저장됩니다. 그리고 .git/index (staging area)에 해당 blob의 해시와 파일 경로가 기록됩니다. 이때 파일의 이전 버전은 그대로 남아있고, 새 버전만 추가됩니다.

Q2. Git 브랜치의 내부 구조를 설명하라.

모범 답변: 브랜치는 .git/refs/heads/ 디렉토리에 있는 40바이트 파일입니다. 이 파일에는 브랜치가 가리키는 커밋의 SHA-1 해시만 저장됩니다. 브랜치 전환은 HEAD 파일을 수정하고, 워킹 트리와 index를 해당 커밋의 tree로 업데이트하는 것입니다.

Q3. merge와 rebase의 내부적 차이를 설명하라.

모범 답변: Merge는 두 브랜치의 최신 커밋과 공통 조상을 사용한 3방향 병합으로 새 merge 커밋을 생성합니다. Rebase는 현재 브랜치의 커밋들을 대상 브랜치 위에 하나씩 재생성합니다. Rebase는 새로운 커밋을 만들기 때문에 SHA-1이 변경됩니다.

Q4. Detached HEAD란 무엇이고, 왜 위험한가?

모범 답변: HEAD가 브랜치를 통하지 않고 직접 커밋을 가리키는 상태입니다. 이 상태에서 새 커밋을 만들면 어떤 브랜치도 이 커밋을 참조하지 않아, 다른 브랜치로 체크아웃하면 해당 커밋에 접근할 수 없게 됩니다. git gc가 실행되면 이러한 미참조 커밋은 삭제됩니다.

Q5. reflog로 삭제된 커밋을 복구하는 방법을 설명하라.

모범 답변: git reflog로 HEAD의 변경 이력을 확인하고, 복구하려는 커밋의 해시를 찾습니다. 그 후 git reset --hard HASH 또는 git checkout -b recovery HASH로 복구합니다. reflog는 기본 30일(미참조) ~ 90일(참조) 보관됩니다.

Q6. pack 파일은 무엇이고, 왜 필요한가?

모범 답변: Git은 초기에 각 객체를 개별 파일(loose object)로 저장하는데, 객체가 많아지면 파일 시스템 성능이 저하됩니다. Pack 파일은 여러 객체를 하나의 파일로 압축하며, delta compression을 통해 비슷한 객체 간 차이만 저장합니다. git gcgit push 시 자동 생성됩니다.

Q7. SHA-1 충돌이 Git에 미치는 영향과 대응은?

모범 답변: SHA-1 충돌 시 서로 다른 내용이 같은 해시를 가져 데이터 무결성이 깨질 수 있습니다. 2017년 SHAttered 공격으로 충돌이 실증되었고, Git은 충돌 감지 로직 추가와 SHA-256 전환을 진행 중입니다. Git 2.29부터 --object-format=sha256 옵션을 지원합니다.

Q8. git clone --depth 1의 내부 동작을 설명하라.

모범 답변: Shallow clone은 히스토리의 일부만 가져옵니다. 서버에서 최신 커밋과 그에 필요한 tree, blob만 pack 파일로 전송합니다. .git/shallow 파일에 shallow boundary 커밋이 기록되어 그 이전 히스토리는 접근할 수 없습니다.

Q9. 3방향 병합 알고리즘을 설명하라.

모범 답변: 두 브랜치의 공통 조상(merge base)을 찾고, base에서 각 브랜치까지의 diff를 계산합니다. 한쪽만 변경한 부분은 자동 적용, 양쪽 모두 같은 부분을 다르게 변경한 경우 충돌로 표시합니다. Git은 git merge-base 명령으로 공통 조상을 찾습니다.

Q10. Plumbing 명령어만으로 커밋을 만드는 과정을 설명하라.

모범 답변: (1) git hash-object -w로 blob 생성, (2) git update-index로 index에 추가, (3) git write-tree로 tree 생성, (4) git commit-tree로 commit 생성, (5) git update-ref로 브랜치가 새 커밋을 가리키게 합니다.


13. 실전 퀴즈 (5문제)

Q1. 다음 중 Git 객체 타입이 아닌 것은? (a) blob (b) tree (c) branch (d) commit (e) tag

정답: (c) branch

브랜치는 Git 객체가 아닙니다. 브랜치는 커밋을 가리키는 참조(reference)로, .git/refs/heads/에 텍스트 파일로 저장됩니다. Git의 4가지 객체 타입은 blob, tree, commit, tag입니다.

Q2. git rebase maingit merge main의 결과가 동일한 경우는?

정답: Fast-forward가 가능한 경우

현재 브랜치가 main에서 분기한 후 main에 추가 커밋이 없는 경우, merge는 fast-forward되고 rebase도 동일한 결과를 냅니다. 두 경우 모두 같은 선형 히스토리가 됩니다.

Q3. git reset --hard HEAD~1 후 삭제된 커밋을 복구하려면?

정답: git reflog에서 삭제된 커밋의 해시를 찾아 git reset --hard HASH 또는 git branch recovery HASH로 복구합니다. reflog는 HEAD의 모든 변경을 기록하므로, reset 전의 커밋 해시가 남아있습니다.

Q4. 같은 내용의 파일이 다른 이름으로 3개 있을 때, Git은 blob을 몇 개 저장하는가?

정답: 1개

Git은 content-addressable storage이므로 파일 내용의 SHA-1 해시를 키로 사용합니다. 같은 내용은 같은 해시이므로 blob 1개만 저장됩니다. 파일명과 경로 정보는 tree 객체에 저장됩니다.

Q5. merge 커밋의 부모가 3개 이상인 경우는?

정답: Octopus merge

git merge branch1 branch2 branch3처럼 3개 이상의 브랜치를 동시에 병합하면 octopus merge가 수행됩니다. 이때 생성되는 merge 커밋은 3개 이상의 부모를 가집니다. 단, 충돌이 발생하면 octopus merge는 실패합니다.


14. 참고 자료

  1. Pro Git Book - Git Internals — 공식 문서
  2. Git from the Bottom Up — John Wiegley
  3. How Git Works Internally — GitHub Blog
  4. Git Object Model — 공식 문서
  5. SHAttered Attack — SHA-1 충돌 실증
  6. Git SHA-256 Transition — SHA-256 전환 계획
  7. Merge Strategies in Git — 공식 문서
  8. Git Rebase Documentation — 공식 문서
  9. Git Internals - Transfer Protocols
  10. Unpacking Git Packfiles — Recurse Center
  11. Git Delta Compression — Matthew McCullough
  12. Think Like a Git — DAG와 그래프 이론 관점에서 Git 이해