Git의 내부 작동 원리를 이해하기 위해 PyVCS를 만들었습니다
요약
Git의 내부 작동 원리를 깊이 있게 이해하기 위해 Python으로 직접 구현한 버전 관리 시스템 PyVCS 제작 과정을 다룹니다. 객체 모델, 인덱스 구조, 스테이징 영역 등 Git의 핵심 메커니즘을 코드로 구현하며 학습한 경험을 공유합니다.
핵심 포인트
- Git의 핵심 객체(blob, tree, commit) 모델 구현
- SHA-1 해싱과 zlib 압축을 이용한 객체 저장 방식 이해
- Git 인덱스(Staging area)의 바이너리 구조 역공학 및 구현
- 직접 만든 VCS를 통해 Git의 내부 동작 원리 체득
내가 이 일을 한 이유
많은 개발자들과 마찬가지로, 저도 매일 Git을 사용합니다. add, commit, push, log 같은 명령어들은 외울 정도로 익숙하지만, 내부적으로 실제로 어떤 일이 일어나는지에 대해서는 막연하게만 알고 있다는 사실을 깨달았습니다. Blob이란 무엇인가? Tree는 Commit과 어떻게 다른가? Index (Staging area)는 어떻게 작동하는가? 그리고 가장 신비롭게도, git push는 실제로 어떻게 원격 서버로 데이터를 전송하는가?
저는 이 미스터리를 풀고 싶었습니다. 지루한 문서를 읽는 대신, 저만의 단순화된 Git 클론을 직접 만들어 보기로 결심했습니다. Git을 대체하려는 것이 아니라, Git을 이해하기 위해서 말이죠.
그래서 저는 Git 기능의 일부를 구현한 순수 Python 버전의 버전 관리 시스템(Version Control System)인 PyVCS를 작성했습니다. 전체 코드베이스는 약 500줄 정도이며, 이는 그 어떤 튜토리얼보다 저에게 더 많은 것을 가르쳐 주었습니다.
내가 만들고자 했던 것
저의 목표는 명확했습니다:
- 객체 저장소 (Object storage) – SHA-1 해싱과 zlib 압축을 사용하여 blob, tree, commit을 저장합니다.
- 스테이징 영역 (Staging area/index) – 전형적인
add및commit흐름을 구현합니다. - 커밋 히스토리 (Commit history) – 부모 포인터(Parent pointers), 로그(Log), 그리고 체인을 따라 탐색(Walk the chain)하는 기능입니다.
- 워킹 트리 작업 (Working tree operations) –
status및diff기능입니다. - 원격 푸시 (Remote push) – HTTP를 통해 Git 팩 프로토콜(Pack protocol)을 사용합니다.
또한, 기능이 제한적이더라도 작업이 끝난 후 실제로 명령줄 도구(Command-line tool)로 사용할 수 있도록 설치 가능하게 만들고 싶었습니다.
1단계: 객체 모델(Object Model) 이해하기
Git의 객체 모델은 놀라울 정도로 우아합니다. 네 가지 유형이 있는데(저는 세 가지를 구현했습니다), blob(파일 내용), tree(디렉토리 목록), 그리고 commit(메타데이터가 포함된 스냅샷)이 있습니다. 각 객체는 헤더(type size 0) 뒤에 데이터가 오는 방식으로 저장되며, zlib으로 압축되고 SHA-1 해시로 이름이 지정됩니다.
제가 가장 먼저 작성한 것은 hash_object()였습니다. 이 함수는 해시를 계산하고, 압축된 데이터를 .vcs/objects/ab/cdef...(2단계 디렉토리 구조)에 기록한 뒤 해시를 반환합니다. 또한 객체를 다시 압축 해제하고 파싱하기 위해 read_object()도 작성했습니다.
내가 직접 만든 객체들이 Git과 정확히 동일한 형식을 갖추고 있다는 것을 확인했을 때의 전율은 대단했습니다. .vcs를 .git으로 이름을 바꾸기만 하면 git cat-file 명령어를 사용하여 그것들을 확인할 수도 있었습니다 (실제로 바꾸지는 않았지만요).
2단계: 인덱스(Index) – 교묘한 데이터 구조
인덱스(.vcs/index)는 어떤 파일들이 스테이징(staged)되었는지와 그 메타데이터를 추적하는 바이너리 파일입니다. 저는 Git 인덱스 형식(버전 2)을 역공학(reverse-engineered)했습니다. 이는 헤더, 가변 길이 경로 이름을 가진 일련의 고정 길이 엔트리(entries), 그리고 마지막에 붙는 SHA-1 체크섬으로 구성됩니다.
struct.unpack을 사용하여 이를 파싱하는 과정은 다소 지루했지만, 일단 정확하게 구현하고 나니 ls-files로 스테이징된 파일 목록을 나열하고, status를 위해 이를 워킹 트리(working tree)와 비교할 수 있었습니다.
까다로운 부분은 인덱스 엔트리로부터 트리(tree)를 구축하는 것이었습니다. Git은 트리가 정렬되어 있어야 하며 중첩된 디렉토리를 처리해야 합니다. 저는 엔트리들을 디렉토리별로 그룹화하고, 서브트리(subtrees)를 구축하며, 최종 트리 객체를 생성하는 재귀적인 build_tree_from_entries() 함수를 작성했습니다. 그것은 명확한 깨달음의 순간이었습니다. Git이 디렉토리를 어떻게 표현하는지 마침내 이해하게 된 것입니다.
3단계: 커밋(Commits)과 마스터 브랜치(Master Branch)
커밋은 트리 해시(tree hash), 부모 해시(parent hash, 있는 경우), 작성자/커미터(author/committer) 정보, 타임스탬프, 그리고 메시지를 포함하는 텍스트 객체일 뿐입니다. 저는 다음을 수행하기 위해 commit()을 작성했습니다:
- 현재 인덱스로부터 트리를 작성합니다.
- 현재
master포인터(.vcs/refs/heads/master에 저장된 참조)를 읽습니다. - 커밋 객체를 생성합니다.
master참조를 새로운 커밋 해시로 업데이트합니다.
그 후 log 명령은 단순히 부모 체인(parent chain)을 따라가며 각 커밋의 세부 정보를 출력합니다. 마치 타임라인이 살아 움직이는 것을 보는 것 같았습니다.
4단계: 거대한 도전 – 푸시(Push)
이 부분이 가장 어려운 부분이었습니다. 저는 로컬 커밋을 HTTP를 통해 원격 Git 저장소(GitHub 등)로 푸시하고 싶었습니다. Git은 두 단계로 이루어진 스마트 프로토콜(smart protocol)을 사용합니다:
- 탐색(Discovery) –
GET /info/refs?service=git-receive-pack이 원격 저장소의 현재 참조(refs)를 반환합니다. - 업로드(Upload) –
POST /git-receive-pack이 원격 저장소에 없는 객체들을 포함하는 팩파일(packfile)과 참조 업데이트 명령을 전송합니다.
저는 다음과 같은 작업을 수행해야 했습니다:
- pkt-line 형식을 파싱 (각 라인 앞에 4바이트 16진수 길이가 접두사로 붙음).
- 로컬 커밋과 원격 커밋 사이의 누락된 객체 집합 계산 (객체 그래프의 재귀적 차이(recursive diff)).
- 팩파일(packfile) 구축 – 헤더, 각 객체에 대한 압축된 객체 데이터, 그리고 마지막 SHA-1이 포함된 커스텀 바이너리 형식.
- 가변 길이 헤더(타입 및 크기)와 압축된 데이터로 객체 인코딩.
팩파일(packfile) 로직이 가장 복잡했습니다. 저는 공식 Git 문서와 xxd 및 hexdump를 이용한 수많은 시행착오를 거쳤습니다. 마침내 서버로부터 unpack ok라는 메시지를 보았을 때, 저는 말 그대로 환호성을 질렀습니다.
단계 5: 설치 가능하게 만들기
핵심 기능이 작동하자, 저는 이를 공유하고 싶었습니다. pyproject.toml과 콘솔 스크립트 엔트리 포인트(entry point)를 갖춘 Python 패키지로 구조화했습니다. 이제 누구나 다음과 같이 설치할 수 있습니다:
pip install pyvcs
그리고 어디에서든 pyvcs init, pyvcs add 등을 실행할 수 있습니다.
배운 점
- Git은 마법이 아니다 – Git은 단순하고 잘 설계된 데이터 구조와 프로토콜의 집합입니다.
- 인덱스(index)는 캐시이다 – 커밋과 차이(diff) 계산 속도를 높이기 위해 파일 메타데이터와 해시를 저장합니다.
- 트리(trees)는 재귀적이다 – 트리 구조는 Git의 빠른 디렉토리 비교를 가능하게 하는 핵심입니다.
- 팩파일(packfiles)은 영리하다 – 객체를 압축하고 중복을 제거하지만, 그 형식은 놀라울 정도로 직관적입니다.
- HTTP는 그저 바이트일 뿐이다 – Git 프로토콜은 단지 라인과 바이너리 청크(chunks)의 스트림입니다. 프레이밍(framing)을 이해하고 나면 전혀 무섭지 않습니다.
그 이상으로, 저는 Git을 빠르고 신뢰할 수 있게 만드는 설계 결정들에 대해 깊은 경외감을 느꼈습니다. Linus Torvalds와 Git 커뮤니티는 진정으로 놀라운 것을 만들어냈습니다.
PyVCS가 할 수 있는 것 (그리고 할 수 없는 것)
할 수 있는 것:
init,add,commit,status,diff,log- 검사를 위한
ls-files및cat-file - 원격 HTTP 저장소로
push(기본 인증 포함)
할 수 없는 것:
- 브랜치(branches) 또는 태그(tags) (오직
master만 가능) pull또는clone(push는 작동하므로 절반의 기능은 갖춘 셈입니다)- 병합(merging) 또는 충돌 해결(conflict resolution)
이것은 실제 서비스용 도구(production tool)로 만들어진 것이 아니라, 교육용 실험 프로젝트입니다. 하지만 기능적으로 작동하며, 제가 Git을 내부부터 완전히 이해하는 데 큰 도움이 되었습니다.
직접 시도해 보세요
저는 GitHub에 MIT 라이선스로 PyVCS를 오픈 소스로 공개했습니다. 버전 관리(version control)가 어떻게 작동하는지 궁금했던 적이 있다면, 코드를 클론(clone)하여 읽어보고, 어쩌면 직접 확장해 보시는 것도 추천합니다.
git clone https://github.com/abdullahkhaver/pyvcs.git
cd pyvcs
pip install -e .
...
작동하는 무언가를 만들어내는 즐거움과 그 과정에서 얻은 통찰력 덕분에, 이 프로젝트는 제가 해온 일 중 가장 보람찬 프로젝트 중 하나가 되었습니다.
만약 여러분만의 VCS를 직접 작성하기로 결정하신다면, 꼭 소식을 들려주세요. 즐거운 해킹(Happy hacking) 되시길 바랍니다!
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기