
Git은 자동으로 변경된 파일을 추적하며, 자유자재로 브랜치를 이동하고 이전 상태로 되돌릴 수 있다.
도대체 어떻게 이런 기능이 가능한 걸까?
Git은 파일 변경의 차이(diff)만 저장하는 게 아니라, 전체 폴더 구조(Tree)와 파일 내용(Blob)의 상태를 통째로 저장하는 스냅샷(snapshot) 방식을 사용한다.
작업 디렉토리의 상태를 git add를 통해 Staing Area에 올리고 git commit을 하면 해당 시점의 디렉토리와 파일 구조 전체를 Tree 객체로 저장하고 이를 참조하는 Commit 객체를 생성한다.
Blob: 파일의 내용을 저장하는 객체로, 파일의 실제 데이터를 고유하게 식별할 수 있는 해시값을 가진다. 파일 내용만 저장되고 파일의 이름이나 경로는 포함하지 않는다.
Tree: 파일 시스템의 폴더 구조를 표현하는 객체로, 하위 Blob 객체(파일 내용)과 다른 Tree 객체(서브 폴더)를 참조한다. 폴더와 파일 간의 관계를 정의한다.
Git은 커밋을 생성할 때, Tree 객체, 메타데이터, 부모 커밋의 해시값을 포함한 전체 커밋 정보를 기반으로 SHA-1 해시를 계산한다.
동일한 내용으로 커밋을 한다면, 동일한 해시값이 생성되어 중복 커밋을 자동으로 방지할 수 있다.
Commit 객체는 부모 커밋의 해시값을 참조한다. 즉, 각 커밋은 바로 전 커밋이 무엇이었는지를 알고 있다.
이러한 연결 덕분에 Git은 과거부터 현재까지의 이력을 선형적으로 재구성할 수 있다.
SHA-1 해시는 데이터를 고유한 고정 길이 값으로 변환하는 함수로, Git에서 각 객체(커밋, 트리, 블롭 등)를 고유하게 식별하는 데 사용된다.
이러한 커밋이 생성되기 전에 Staging Area(Index)라는 중간 영역을 거친다.
Staging Area는 변경된 파일 중 어떤 파일을 커밋에 포함시킬지 선택적으로 준비하는 공간이다.
git add를 실행하면, Git은 해당 파일의 현재 상태에 대한 Blob 객체(파일 내용의 스냅샷)를 생성하여 Staging Area에 임시 저장한다.
이 단계에서는 Tree 객체나 Commit 객체는 생성되지 않으며, Git은 준비된 Blob들을 내부적으로 기록하고, 기존 상태 반영할 준비만 한다.
이후 git commit 명령어를 실행하면, Staging Area에 있는 Blob들을 기반으로 Tree 객체가 구성되고, 이 Tree를 참조하는 Commit 객체가 생성되며, 최종적으로 Git 디렉토리(.git)에 저장된다.
Git에서 브랜치(branch)는 단순히 하나의 커밋을 가리키는 포인터에 불과하다.
우리가 흔히 알고 있는 main 브랜치나 feature/login 같은 이름들은 사실 특정 커밋의 SHA-1 해시를 가리키는 라벨(label)이다.
브랜치를 새로 만들면 기존 커밋에서 새로운 포인터가 생성된다. 이후 새로운 커밋이 추가되면, 해당 브랜치 포인터가 최신 커밋을 따라 자동으로 이동하게 된다.
이 덕분에 Git은 서로 다른 작업 흐름을 브랜치 단위로 분리해 진행할 수 있으며, 병합(merge)이나 리베이스(rebase) 등을 통해 브랜치 간 이력을 통합할 수 있다.
브랜치를 옮긴다는 건, HEAD 포인터가 가리키는 브랜치를 바꾸는 것이다.
HEAD -> 브랜치 -> 커밋의 연결 구조를 통해, 작업할 브랜치를 손쉽게 전환하고 관리할 수 있다.