01장에서 Git이 델타가 아니라 스냅샷을 저장한다고 설명했고, 03장에서는 .git/objects 디렉터리가 그 스냅샷들을 담는다고 언급했다. 7부의 첫 챕터인 이 장은 그 안을 직접 열어, 지금까지 다룬 명령들이 실제로 어떤 데이터 구조를 조작해온 것인지 확인한다.
개요
Git이 저장하는 객체는 정확히 세 가지 유형이며, 각각 다른 것을 표현한다.
| 객체 유형 | 표현하는 것 |
|---|---|
| blob | 파일 하나의 내용(파일 이름·경로 정보는 포함하지 않음) |
| tree | 디렉터리 하나의 구조(파일 이름과 그 파일이 가리키는 blob, 하위 디렉터리가 가리키는 다른 tree) |
| commit | 특정 시점의 tree(전체 스냅샷)를 가리키는 포인터 + 부모 커밋 + 작성자·메시지(08장) |
모든 객체는 내용을 SHA-1 알고리즘으로 해시한 40자 16진수 문자열로 식별되며, 이 값이 곧 그 객체의 “주소"다. git add(05장)를 실행하면 파일 내용이 blob으로 저장되고, git commit(08장)을 실행하면 스테이징 영역의 상태를 나타내는 tree와, 그 tree를 가리키는 commit 객체가 함께 만들어진다.
기본 개념
이 세 객체가 어떻게 서로를 가리키는지는 문단보다 그림으로 봐야 관계가 명확하다 — commit이 tree를 가리키고, tree가 다시 다른 tree나 blob을 가리키는 계층 구조이기 때문이다.
flowchart TB
commit["commit 객체(작성자, 메시지, 부모 커밋)"] --> rootTree["tree 객체(루트 디렉터리)"]
rootTree --> readmeBlob["blob(README.md 내용)"]
rootTree --> srcTree["tree 객체(src/ 디렉터리)"]
srcTree --> appBlob["blob(app.js 내용)"]
srcTree --> utilBlob["blob(util.js 내용)"]
git commit(08장)이 새 커밋을 만들 때마다 이 그림 전체가 매번 통째로 새로 만들어지는 것은 아니다 — 만약 한 커밋에서 README.md만 수정했다면, 새 commit은 새 rootTree를 가리키지만 그 rootTree 안의 srcTree(내용이 바뀌지 않았으므로)는 이전 커밋과 완전히 동일한 tree 객체를 그대로 재사용한다. 01장에서 언급했던 “바뀌지 않은 파일은 참조로 재사용해 중복 저장을 피한다"는 설명이 바로 이 구조에서 나온다.
종류/세부
git cat-file로 객체 내용 직접 확인하기
Git은 이 저수준 객체를 직접 조회하는 명령을 제공한다.
| |
commit 객체를 -p로 열어보면 08장에서 설명한 구성 요소(tree 해시, parent 해시, author, committer, 메시지)가 그대로 텍스트로 보인다.
| |
tree 객체를 -p로 열어보면 그 디렉터리 안의 각 항목이 파일 모드, 객체 유형, 해시, 이름 순으로 나열된 목록이 나온다.
| |
브랜치·태그는 객체가 아니라 참조다
11장·22장에서 다룬 브랜치와 태그는 이 세 객체 유형에 포함되지 않는다 — 이들은 .git/refs/ 아래의 일반 파일로, 특정 commit 객체의 해시 하나만 담고 있는 참조(reference)일 뿐이다. 이 구분은 35장에서 refs와 HEAD를 다룰 때 더 자세히 살펴본다. (참고로 주석 달린 태그는 예외적으로 태그 자체를 위한 네 번째 객체 유형(tag 객체)을 추가로 만든다는 점을 22장에서 이미 언급했다.)
압축과 저장 위치
방금 만든 객체는 .git/objects/ 아래 해시의 앞 2자를 디렉터리 이름으로, 나머지 38자를 파일 이름으로 압축 저장된다(예: 해시가 a1b2c3d...라면 .git/objects/a1/b2c3d...). 이렇게 흩어져 저장된 개별(loose) 객체들은 나중에 36장에서 다룰 git gc가 packfile이라는 압축된 단일 파일로 묶어 저장 효율을 높인다.
주의사항·함정
같은 내용의 파일은 저장소 어디에 있든 같은 blob을 공유한다: blob은 파일 경로나 이름을 전혀 포함하지 않고 오직 내용만으로 해시가 결정되므로, 완전히 다른 두 디렉터리에 있는 내용이 똑같은 파일 두 개는 저장소 안에서 동일한 blob 객체 하나를 가리킨다. 이는 버그가 아니라 저장 공간을 아끼기 위한 의도된 설계다.
SHA-1 해시 충돌 우려는 실무에서 사실상 무시해도 좋은 수준이다: 이론적으로 서로 다른 두 내용이 같은 SHA-1 해시를 가질 가능성이 존재하지만, 그 확률은 실무에서 우연히 마주칠 가능성이 거의 없을 정도로 낮다. Git 프로젝트는 이후 버전에서 더 강력한 해시 알고리즘(SHA-256)으로의 전환 옵션도 도입했지만, 이는 이 컬렉션의 범위를 넘는 고급 주제다.
objects 디렉터리를 직접 수정하려 하지 않는다: 위에서 다룬 cat-file은 읽기 전용 조회 도구다. 이 디렉터리의 파일을 직접 편집하거나 삭제하면 저장소가 손상될 수 있으며, 객체를 다루는 모든 정상적인 조작은 git add, git commit 같은 상위 명령을 통해야 한다.
![Featured image of post [Git] 34. Git 객체 모델 — blob/tree/commit](/post/git/git-object-model-blob-tree-commit/wordcloud_hu_6efec49067f523ee.webp)
![[Git] 32. git blame — 변경 이력 추적](/post/git/git-blame-command-track-line-history/wordcloud_hu_249514842ed0d027.webp)
![[Git] 33. git bisect — 버그 커밋 이분 탐색](/post/git/git-bisect-command-binary-search-bug-commit/wordcloud_hu_8196480e8b21de7a.webp)
![[Git] 34. Git 객체 모델 — blob/tree/commit](/post/git/git-object-model-blob-tree-commit/wordcloud_hu_bd4ee27759fca634.webp)
![[Git] 35. Git refs와 HEAD](/post/git/git-refs-and-head-reference-model/wordcloud_hu_7b81aa541bd4941d.webp)
![[Git] 36. Packfile과 git gc](/post/git/packfile-and-git-gc/wordcloud_hu_b3276dfd3f3cb2be.webp)
![[Git] 04. 작업 트리·스테이징 영역·저장소 3단계 모델](/post/git/git-three-stage-model-working-tree-staging-repository/wordcloud_hu_59d756dc08aaa5c.webp)
![[Git] 00. 과정 개요와 커리큘럼](/post/git/getting-started-git/wordcloud_hu_35c98b09a46a4d09.webp)
![[Git] 10. .gitignore — 추적 제외 규칙](/post/git/gitignore-file-exclude-patterns/wordcloud_hu_cd51113edaa1c4db.webp)
![[Git] 12. git switch/checkout — 브랜치 전환](/post/git/git-switch-checkout-branch-transition/wordcloud_hu_cfa8ddb8839c8503.webp)