본문 바로가기
TYLER SONGBlog
← Blog index
CS학습5 min read

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는 현재 체크아웃한 브랜치(또는 커밋)를 가리킨다.

text
HEAD → main → C3 → C2 → C1

3.4 파일의 상태 흐름

text
작업 디렉터리 --git add--> 스테이징 영역 --git commit--> 저장소

파일은 untracked(추적 안 됨), modified(수정됨), staged(스테이징됨), committed(커밋됨) 상태 사이를 이동한다.

4. 코드 예제

새 저장소를 만들고 커밋한 뒤, 내부 객체를 직접 확인하는 과정이다.

shell
# 실습용 임시 디렉터리에서 시작 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일 수 있음)

스테이징 영역을 활용해 일부만 커밋하는 예시이다.

shell
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 디렉터리를 직접 확인해 보면 구조가 명확하게 이해된다.