Git 기본 구조: 작업 디렉터리, 스테이징 영역, 저장소의 3단계와 객체 모델
Git은 파일의 변경 이력을 스냅샷(snapshot) 단위로 저장하는 분산 버전 관리 시스템(DVCS)이다. 작업 디렉터리(working directory), 스테이징 영역(staging area), 저장소(repository)의 세 영역을 오가며 커밋을 만든다. 내부적으로는 blob, tree, commit, tag 네 가지 객체를 내용 기반 해시로 저장
By 송민성
1. 개념
Git은 소스 코드의 변경 이력을 기록하고 되돌릴 수 있게 하는 분산 버전 관리 시스템(Distributed Version Control System, DVCS)이다. 각 개발자의 로컬에 전체 이력을 가진 저장소가 있으므로, 네트워크 없이도 커밋, 브랜치 생성, 이력 조회가 가능하다.
Git의 기본 구조는 두 층으로 나눠 볼 수 있다.
사용자가 다루는 세 영역
| 영역 | 설명 | 위치 |
|---|---|---|
| 작업 디렉터리(Working Directory) | 실제로 파일을 편집하는 공간 | 프로젝트 폴더 |
| 스테이징 영역(Staging Area, Index) | 다음 커밋에 포함할 변경을 모아두는 공간 | .git/index |
| 저장소(Repository) | 커밋된 이력이 저장되는 공간 | .git/objects |
내부에 저장되는 네 가지 객체(Object)
- blob: 파일 내용
- tree: 디렉터리 구조 (파일명과 blob, 하위 tree를 가리킴)
- commit: 특정 시점의 tree, 부모 커밋, 작성자, 메시지
- tag: 특정 객체에 붙인 이름표 (주석 태그
annotated tag의 경우)
2. 왜 사용하는가
- 이력 추적: 누가, 언제, 왜 코드를 바꿨는지 기록으로 남는다.
- 안전한 실험: 브랜치(branch)를 만들어 기존 코드에 영향 없이 작업하고, 마음에 들지 않으면 버릴 수 있다.
- 협업: 여러 사람이 동시에 작업한 변경을 병합(merge)할 수 있다.
- 복구: 잘못 수정한 파일을 과거 시점으로 되돌릴 수 있다.
스테이징 영역이 있는 이유도 여기에 있다. 작업 중인 변경 중 일부만 골라 의미 단위로 커밋할 수 있기 때문에, 이력이 "한 가지 목적의 변경" 단위로 깔끔하게 유지된다.
3. 동작 원리
3.1 스냅샷 방식
Git은 커밋할 때마다 "변경된 줄의 차이"가 아니라 그 시점의 프로젝트 전체 상태를 가리키는 tree를 기록한다. 내용이 바뀌지 않은 파일은 같은 blob을 재사용하므로 저장 공간을 낭비하지 않는다. (저장 시 packfile 단계에서 델타 압축이 별도로 적용된다.)
3.2 내용 주소 지정(Content-addressable) 저장
모든 객체는 내용으로 계산한 해시를 이름으로 삼는다. 같은 내용이면 같은 해시가 나오고, 내용이 한 글자라도 다르면 해시가 달라진다. 기본 해시 알고리즘은 SHA-1이며, SHA-256 저장소 형식도 지원한다.
객체는 .git/objects/ 아래에 해시 앞 2글자를 디렉터리명, 나머지를 파일명으로 하여 저장된다.
3.3 커밋의 연결
각 commit 객체는 부모 커밋의 해시를 포함한다. 따라서 커밋은 방향성 비순환 그래프(DAG, Directed Acyclic Graph)를 이룬다. 브랜치는 특정 커밋을 가리키는 포인터(참조, ref)일 뿐이며, HEAD는 현재 체크아웃한 브랜치(또는 커밋)를 가리킨다.
HEAD → main → C3 → C2 → C13.4 파일의 상태 흐름
작업 디렉터리 --git add--> 스테이징 영역 --git commit--> 저장소파일은 untracked(추적 안 됨), modified(수정됨), staged(스테이징됨), committed(커밋됨) 상태 사이를 이동한다.
4. 코드 예제
새 저장소를 만들고 커밋한 뒤, 내부 객체를 직접 확인하는 과정이다.
# 실습용 임시 디렉터리에서 시작
mkdir git-demo && cd git-demo
git init
git config user.name "Demo"
git config user.email "demo@example.com"
# 1) 작업 디렉터리에서 파일 생성
echo "hello git" > hello.txt
git status --short # ?? hello.txt (untracked)
# 2) 스테이징 영역에 추가
git add hello.txt
git status --short # A hello.txt (staged)
# 3) 저장소에 커밋
git commit -m "첫 커밋: hello.txt 추가"
# 4) 이력 확인
git log --oneline
# 5) 내부 객체 확인
git cat-file -t HEAD # commit
git cat-file -p HEAD # tree 해시, author, 메시지 출력
git cat-file -p 'HEAD^{tree}' # tree가 가리키는 blob 목록
git cat-file -p HEAD:hello.txt # blob 내용: hello git
# 6) 저장된 객체 파일 목록
find .git/objects -type f
# 7) 포인터 확인
cat .git/HEAD # ref: refs/heads/main (초기 브랜치명 설정에 따라 master일 수 있음)스테이징 영역을 활용해 일부만 커밋하는 예시이다.
echo "a" > a.txt
echo "b" > b.txt
git add a.txt # a.txt만 스테이징
git commit -m "a.txt만 커밋"
git status --short # ?? b.txt 가 남아 있음5. 시간 복잡도 또는 성능 특성
- 객체 조회는 해시를 키로 하므로 loose 객체는 경로 계산으로 바로 접근할 수 있고, packfile은 인덱스(
.idx)를 이용해 찾는다. - 브랜치 생성은 41바이트 정도의 ref 파일(해시 40자 + 개행)을 쓰는 작업이므로 저장소 크기와 무관하게 매우 가볍다. (SHA-1 기준)
git status는 작업 디렉터리와 index를 비교하므로 추적 중인 파일 수에 영향을 받는다. 파일이 아주 많은 대형 저장소에서는 느려질 수 있다.- 오래된 loose 객체는
git gc로 packfile에 묶어 공간과 성능을 개선할 수 있다.
6. 실무 사용 사례
- 기능 단위 브랜치 개발:
feature/login브랜치에서 작업하고 Pull Request로 병합한다. - 의미 단위 커밋:
git add -p로 변경의 일부만 골라 "버그 수정"과 "리팩터링"을 별도 커밋으로 분리한다. - 원인 추적:
git log,git blame,git bisect로 버그가 들어온 커밋을 찾는다. - 무시 파일 관리:
.gitignore에node_modules/,.env, 빌드 산출물을 등록해 추적 대상에서 제외한다.
7. 주의할 점
git add는 "파일 추가"가 아니라 "그 시점의 내용을 스테이징"하는 명령이다. add 이후 파일을 또 수정하면 수정분은 스테이징되지 않는다.- Git과 GitHub는 다르다. GitHub는 Git 저장소를 호스팅하는 서비스이다.
- 커밋된 내용은 비교적 안전하지만, 커밋하지 않은 변경은
git checkout -- <파일>,git restore,git reset --hard등으로 사라질 수 있다. - 비밀번호, API 키를 한 번 커밋하면 이후 파일을 지워도 이력에 남는다. 유출 시 키를 폐기(revoke)하는 것이 우선이다.
- 이미 원격에 공유한 커밋을
rebase나reset으로 고쳐 강제 푸시(force push)하면 협업자의 이력과 충돌한다.
8. 핵심 정리
- Git은 작업 디렉터리 → 스테이징 영역 → 저장소의 3단계로 변경을 기록한다.
- 내부 객체는 blob, tree, commit, tag이며, 내용 기반 해시로 식별된다.
- 커밋은 부모를 가리키는 DAG를 이루고, 브랜치와 HEAD는 커밋을 가리키는 포인터이다.
- 스테이징 영역 덕분에 변경을 의미 단위로 나눠 커밋할 수 있다.
git cat-file과.git디렉터리를 직접 확인해 보면 구조가 명확하게 이해된다.