Search

Pro Git 1장 - 시작하기

Git을 처음 접한지 어느새 8년 정도 되었고 add, commit, push, pull과 같은 명령어는 일상적으로 사용해왔습니다. 그러나 Git이 내부적으로 어떻게 동작하는지, 왜 다른 버전 관리 시스템과 차별화되는지에 대한 깊은 이해는 부족했습니다. 이번에 Pro Git 책을 통해 Git의 내부 구조와 작동 원리를 심도 있게 공부하면서 새롭게 알게 된 내용과 추가 조사를 통해 얻은 내용을 정리합니다.

버전 관리 시스템의 진화: 로컬 → 중앙집중식 → 분산식

버전 관리 시스템은 시간에 따라 크게 세 가지 형태로 발전해왔습니다.

로컬 버전 관리

초기 버전 관리는 단순한 로컬 데이터베이스에 파일의 변경 정보(Patch Set)를 저장하는 방식이었습니다. RCS(Revision Control System)가 대표적인 예로, 변경된 부분을 특별한 형식으로 저장하고 이를 순차적으로 적용하여 특정 시점의 파일을 복원할 수 있었습니다. 그러나 이 방식은 협업이 어렵다는 치명적인 한계가 있었습니다.

중앙집중식 버전 관리 시스템(CVCS)

CVS, Subversion, Perforce와 같은 중앙집중식 시스템은 파일을 관리하는 중앙 서버와 이를 사용하는 클라이언트로 구성됩니다. 많은 사람들이 CVCS와 DVCS의 차이를 단순히 '중앙 서버가 있느냐 없느냐'로 이해하곤 하지만, 실제 핵심적인 차이는 저장소 복제 방식히스토리 관리에 있습니다.
CVCS에서는:
사용자가 최신 버전의 파일만 체크아웃하여 로컬에 가져옵니다.
전체 이력과 메타데이터는 중앙 서버에만 존재합니다.
로컬에는 작업 중인 파일의 현재 스냅샷만 있을 뿐, 프로젝트의 전체 히스토리는 없습니다.
따라서 서버가 다운되면 새 커밋을 만들 수는 있어도, 이전 커밋 이력 조회, 브랜치 변경, 병합 등의 대부분의 버전 관리 작업은 불가능합니다. 이는 단순히 '작업은 계속하고 나중에 보고'할 수 있는 수준을 넘어 심각한 제약이 됩니다.

분산 버전 관리 시스템(DVCS)

Git, Mercurial 등의 DVCS는 근본적으로 다른 접근 방식을 취합니다:
저장소를 클론할 때 전체 프로젝트 이력과 메타데이터를 모두 로컬로 가져옵니다.
모든 사용자가 저장소의 완전한 사본을 가지고 있습니다.
오프라인 상태에서도 이력 조회, 브랜치 변경, 커밋 등 거의 모든 작업이 가능합니다.
서버가 다운되어도 모든 클라이언트가 전체 이력을 가지고 있어 복원이 가능합니다.
이러한 차이가 Git의 핵심 설계 철학과 깊이 연결되어 있습니다.

Git의 핵심 데이터 모델: 스냅샷의 스트림

Git의 가장 중요한 특징 중 하나는 데이터를 저장하는 방식입니다. 대부분의 버전 관리 시스템(CVS, Subversion 등)은 델타 기반 접근법을 사용합니다. 이 방식에서는:
초기 파일 버전을 저장한 후 각 변경사항(델타)만 순차적으로 저장합니다.
특정 버전의 파일을 가져오려면 초기 버전부터 모든 변경사항을 차례로 적용해야 합니다.
반면 Git은 스냅샷의 스트림으로 데이터를 관리합니다:
각 커밋마다 프로젝트 전체의 스냅샷을 저장합니다.
변경되지 않은 파일은 새로 저장하지 않고, 이전 스냅샷의 동일 파일에 대한 링크만 저장합니다.
이 개념은 처음에는 비효율적으로 보일 수 있습니다. 파일의 모든 버전을 통째로 저장한다면 저장 공간이 기하급수적으로 증가할 것 같기 때문입니다. 그러나 Git의 실제 구현은 이보다 훨씬 더 정교합니다.

Git의 주요 구성 요소와 작동 방식

Git을 이해하기 위해서는 몇 가지 핵심 구성 요소를 알아야 합니다:

객체 데이터베이스(Object Database)

Git의 핵심 저장소로, 모든 버전의 파일 내용, 디렉토리 구조, 커밋 정보가 저장됩니다. 이 데이터베이스는 네 가지 유형의 객체로 구성됩니다:
1.
Blob: 파일 내용을 저장합니다. 파일명이나 권한 정보는 포함하지 않고 순수 내용만 저장합니다.
2.
Tree: 디렉토리 구조를 나타냅니다. 파일명, 권한, blob 또는 다른 tree 객체에 대한 참조를 포함합니다.
3.
Commit: 특정 시점의 스냅샷을 나타냅니다. 최상위 tree, 부모 commit(들), 작성자, 커밋 메시지 등을 포함합니다.
4.
Tag: 특정 commit에 이름을 붙인 것입니다.
모든 객체는 SHA-1 해시로 식별되며, 이 해시는 객체의 내용을 기반으로 생성됩니다. 이러한 내용 기반 주소 지정(content-addressed storage) 방식은 Git의 무결성을 보장하는 핵심 메커니즘입니다.

Git 디렉토리(.git)

프로젝트를 복제하거나 초기화할 때 생성되는 숨겨진 디렉토리입니다. 이 디렉토리에는:
객체 데이터베이스(.git/objects)
브랜치와 태그 정보(.git/refs)
HEAD 참조(현재 체크아웃된 브랜치)
설정 정보(.git/config) 등이 포함됩니다.

워킹 트리(Working Tree)

실제로 작업하는 프로젝트 디렉토리입니다. Git 디렉토리의 압축된 데이터베이스에서 특정 버전의 파일을 추출하여 디스크에 펼쳐놓은 것으로 볼 수 있습니다. 파일을 수정하면 워킹 트리에서 변경이 이루어집니다.

스테이징 영역(Staging Area/Index)

다음 커밋에 포함될 변경사항을 준비하는 중간 영역입니다. 기술적으로는 .git 디렉토리 안의 index 파일입니다. git add 명령을 실행하면 워킹 트리의 변경사항이 이 영역에 추가됩니다.
이러한 세 가지 영역(Git 디렉토리, 워킹 트리, 스테이징 영역)의 상호작용을 통해 Git의 세 가지 상태(Committed, Modified, Staged)가 관리됩니다.

Git의 효율적인 저장 메커니즘

앞서 언급했듯이 Git은 각 커밋마다 프로젝트의 스냅샷을 저장합니다. 그러나 이것이 비효율적이지 않은 이유는 Git이 사용하는 여러 최적화 기법 때문입니다:

1. 내용 기반 저장(Content-Based Storage)

Git은 파일의 내용을 해시하여 저장합니다. 동일한 내용의 파일은 해시값이 같아 한 번만 저장됩니다. 이는 다음과 같은 이점을 제공합니다:
파일 이름이 바뀌어도 내용이 같으면 추가 저장공간이 필요 없습니다.
서로 다른 브랜치에 동일한 파일이 있어도 한 번만 저장됩니다.
한 파일의 내용이 여러 커밋에서 변하지 않으면 계속 같은 객체를 참조합니다.

2. 팩 파일(Pack Files)

Git은 주기적으로(특히 git gc 명령 실행 시) "가비지 컬렉션"을 수행합니다. 이 과정에서:
개별 객체들을 "팩 파일"이라는 압축된 형태로 묶습니다.
팩 파일 내에서는 델타 압축 기법을 사용하여 유사한 파일들 간의 차이만 저장합니다.
이는 일견 모순되어 보일 수 있습니다. Git이 델타 기반이 아닌 스냅샷 기반이라고 했는데, 왜 팩 파일에서는 델타 압축을 사용할까요? 이는 개념적 모델과 실제 구현의 차이입니다:
개념적으로 Git은 각 커밋을 독립적인 스냅샷으로 취급합니다.
그러나 저장 효율성을 위해 내부적으로는 델타 압축을 사용합니다.
중요한 점은 이 델타 압축이 파일 접근에 영향을 주지 않는다는 것입니다. Git은 필요할 때 객체를 효율적으로 재구성할 수 있습니다.

3. zlib 압축

모든 Git 객체는 zlib 알고리즘으로 압축되어 저장됩니다. 텍스트 파일(코드, 설정 파일 등)의 경우 압축률이 약 70-90%로 매우 높습니다. 실제 zlib 벤치마크 결과를 확인해보면 평균적으로 75% 정도의 높은 압축률을 보여줍니다.

대형 프로젝트에서의 Git 저장 공간 관리

20GB 규모의 대형 프로젝트를 Git으로 관리한다면 저장소 크기는 어떻게 될까요?

실제 저장소 크기에 영향을 미치는 요소

1.
실제 변경 패턴:
대부분의 커밋에서는 전체 프로젝트 중 극히 일부 파일만 변경됩니다.
20GB 프로젝트라도 각 커밋마다 평균적으로 수 KB~수 MB 정도만 변경되는 경우가 일반적입니다.
2.
파일 유형에 따른 압축률 차이:
텍스트 파일(코드, 설정 파일 등)은 압축률이 매우 높습니다.
바이너리 파일은 이미 압축된 경우가 많아 추가 압축 효과가 제한적입니다.
3.
팩 파일의 델타 압축 효과:
같은 파일의 여러 버전 사이에서는 델타 압축 효율이 매우 높습니다.

대형 프로젝트 관리 방법

그럼에도 불구하고 바이너리 파일이 많거나 자주 변경되는 대형 프로젝트에서는 Git 저장소가 실제로 매우 커질 수 있습니다. 이런 경우 일반적으로 다음과 같은 해결책을 사용합니다:
1.
Git LFS(Large File Storage):
대용량 파일은 별도 서버에 저장하고 참조만 Git에 유지합니다.
이미지, 비디오 등 대형 에셋에 일반적으로 사용됩니다.
2.
깊이 제한 클론(shallow clone):
git clone --depth=1과 같이 최근 커밋만 가져오는 방식을 사용합니다.
전체 이력이 필요 없는 경우 저장 공간을 절약할 수 있습니다.
3.
적절한 .gitignore 설정:
빌드 결과물, 임시 파일 등 버전 관리가 불필요한 파일을 제외합니다.
Linux 커널이나 Chromium 같은 대형 프로젝트의 Git 저장소는 수 GB에 달하며, 이는 저장소가 커질 수 있음을 보여줍니다. 하지만 일반적으로 코드 중심 프로젝트에서는 Git의 압축 및 최적화가 상당히 효과적으로 작동합니다.

결론

Git은 단순한 버전 관리 도구를 넘어 정교한 내부 구조와 최적화 메커니즘을 갖추고 있습니다. 스냅샷의 스트림이라는 개념적 모델과 내용 기반 저장, 효율적인 압축 기법의 조합은 Git이 빠른 속도와 공간 효율성을 동시에 달성할 수 있게 합니다.
Git을 더 효과적으로 사용하기 위해서는 이러한 내부 동작 원리를 이해하는 것이 중요합니다. 일상적인 명령어 사용을 넘어 Git의 내부 구조를 알게 되면, 문제 해결 능력이 향상되고 더 복잡한 워크플로우를 구현할 수 있게 됩니다.