백엔드

Git의 내부구조

CyberI 2016. 5. 20. 17:02

우리는 협업을 위해 여러지 형상관리 툴들을 사용합니다. 특히 Git은 최근 개발자들 사이에서는 모르는 사람이 없을 정도로 보편화되고 성능이 좋은 협업/형상관리 툴일것입니다. 본 포스팅에서는 이렇게 많이 사용되는 Git이 어떤 내부구조와 모델을 가지고 작동되는지 개략적으로 살펴보도록 하겠습니다.

다른 형상관리시스템과 비교한 Git의 특징과 장점은 정리된 많은 글 들이 있으니 이 포스팅에서는 다루지 않고 오직 Git의 내부적 구조에 대해서만 집중하도록 하겠습니다. 따라서 Git의 사용법에 대해 알기를 원하시는 분들에게는 약간 불친절한 글이 될 수 있다는 점 먼저 양해부탁드립니다.


진행하기에 앞서 데모를 위한 Git repository를 임의로 생성하도록 하겠습니다.

위와 같이 저장소를 init한 후 README 파일을 생성하여 git add명령을 실행한 상태로 설명하겠습니다.


Object

Git에서 object는 Git을 구성하는 데이터파일입니다. Git은 Working directory의 파일정보를 object형식으로 변환하여 Object database({Working Directory}/.git/object)에 저장합니다.

각각의 Object파일은 Zlib을 이용하여 압축되며, 파일의 내용과 헤더를 40자의 SHA-1 해시값으로 저장합니다.

Object의 종류에는 blob, tree, commit, tag가 있습니다.


- Blob

Working directory의 파일에 대응하여 내용이 저장되는 Object입니다. 중요한것은 Blob에는 파일의 이름이나 형식등은 저장이 되지 않고 파일의 내용만 저장이 된다는 것입니다. 이는 만약 이름이 다른 2개의 파일이 프로젝트 내에 있다해도, 그 내용이 같으면 Git은 Blob을 한개만 저장한다는 것을 의미합니다. 또한, clone이나 fetch등을 할 때에도 파일이 한개만 전송이 될 것입니다.

그러면 실제로 Blob파일이 저장된 것을 확인해 보도록 하겠습니다.

.git/objects/ 디렉토리를 확인해 보면 하위 디렉토리를 3개 확인할 수 있습니다. 이중 info, pack은 git init을 할때 Git이 생성해주는 디렉토리이므로 제외하고 81/디렉토리 내부로 들어가 확인해보면 38자리 파일명의 파일이 생성된것이 확인됩니다. Git은 40자리 해시를 생성할 때 앞의 2자리를 디렉토리명으로 만들어 파일을 생성합니다.

즉 실제로 생성된 Blob의 이름은 '8178c76d627cade75005b40711b92f4177bc6cfc'입니다.

Git의 cat-file명령으로 저장한 데이터를 확인할 수 있습니다.

cat-file-p옵션은 파일의 내용을 확인할 수 있는 옵션이고 -t옵션은 타입을 확인할 수 있는 옵션입니다. README file의 내용인 'readme'문자열과 Object의 타입인 'blob'이 확인됩니다.


- Tree

Working directory의 디렉토리에 대응하여 Git에 저장되는 object입니다. Tree의 내용은 해당 디렉토리 내부의 파일과 디렉토리의 정보(파일명, 형식, SHA-1, 등..)를 담은 blob과 tree object의 리스트입니다.

Tree object를 확인하기 위해서 현재까지의 staging area를 commit한 후 object 디렉토리 내부를 살펴보니 처음 생성되어있던 81/디렉토리가 아닌 2개의 디렉토리가 더 생성된것을 확인할 수 있습니다.

그 중 Tree object를 찾아서 cat-file을 통해 살펴보았습니다. Type이 tree이며, 내용은 README 파일의 정보를 담고 있습니다. 즉 Working directory의 루트 디렉토리정보를 가진 Tree object인것을 알 수 있습니다.


- Commit

이제 방금전에 확인해 보지 않았던 나머지 object하나를 확인해 보도록 하겠습니다.

이 Object는 commit history를 저장하는 object입니다. 파일의 내용을 살펴보면 author와 committer, commit message를 포함하고 있습니다. 또한 내용 제일 첫번째줄을 보면 tree object에 대한 정보가 있는데, 이는 이 commit의 스냅샷의 최상단 tree를 가리키는 포인터입니다. 자세히 살펴보면 우리가 위에서 살펴봤던 tree object(Root)의 SHA-1값을 가리키고 있는것을 알 수 있습니다

파일을 하나 더 생성해서 한번 더 commit 해보겠습니다.

test.txt파일을 생성, commit한 후 해당 commit object를 찾아서 내용을 살펴본 결과입니다. 아까와는 다른 항목이 하나 추가되었는데, parent가 바로 그것입니다. 이 항목은 해당 commit의 바로 직전 commit의 SHA-1값을 가리키는 포인터입니다. 이 포인터를 통해 Git은 commit의 부모를 참조할 수 있습니다.


- Tag

Git의 특정 commit에 tag를 달면 tag object가 생성됩니다. 태그는 Lightweight와 Annotated태그로 나뉘는데 Lightweight태그는 단순히 특정 commit에 대한 포인터로 작동하는 반면, Annotated태그는 태그의 작성자, 이메일, 날짜, 메세지를 저장할 수 있습니다. 또한 보안을 위해 GPG로 서명할 수도 있습니다.


References

Git의 object는 한번 생성되면 그 값이 변할 수 없습니다. 대응되는 파일이 수정되면 다시 새로운 object가 생성이 될 뿐입니다. 또한 40자리의 SHA-1코드는 접근하기가 어렵습니다. 이러한 이유때문에 reference가 존재합니다. Reference는 특정 commit을 가리키는 포인터라는 점에서 tag object와 비슷하지만, object와는 달리 reference는 그 값이 계속해서 바뀔 수 있습니다. 우리가 Git에서 사용하고 있는 branchremote, HEAD같은 요소들을 reference라고 합니다.


Data Model

지금까지 설명하면서 생성된 Git object들을 도식화 하면 다음과 같습니다. 

README파일은 처음 commit이후 변하지 않았기 때문에 두번째 commit의 최상의 tree가 첫번째와 같은 blob을 가리키고 있고 두번째 commit에서는 새로 생성된 test.txt만 추가되었습니다. 그 두번째 커밋을 master branch reference가 가리키고 있고 그 reference를 또 다시 HEAD reference가 바라보고 있습니다.

만약 이 상태에서 /dir/test.txt가 추가되고, README파일이 수정된다면 아래와 같은 형태로 변할것입니다.

Commit object, root tree object가 새로 생성되어 새로 생성된 dir tree object, README, dir/text blob object들을 바라보고 있습니다. 


Git은 이런 방식으로 commit이 될 때 마다 비순환 방향 그래프(Directed Acyclic Graph)를 만들어 갑니다.


- Branch

위와 같은 저장소에서 master가 아닌 새로운 branch를 만들었을 때의 Git 모델을 살펴보겠습니다. 

dev라는 브랜치를 git branch dev명령을 통해 만들고 checkout한 후 README파일을 수정, commit했다고 가정하겠습니다. 아래 그 결과를 도식화 했습니다.

그림이 좀 복잡해졌습니다만, 찬찬히 살펴보면 변한건 commit이 하나 추가된 것 뿐입니다. 이 때, 새로운 commit object와 그에 대응하는 tree object가 추가되었고, README파일 내용이 수정되었기 때문에 README blob object도 하나 새로 생성되었습니다. 나머지는 변함이 없기 때문에 포인터들이 그대로 가리키고 있습니다. 


- Merge

이 상태에서 master branch로 돌아와서 dev를 merge하게 되면, 아래와 같은모양이 됩니다.

(아래부터 모형이 복잡해질 수 있으니 tree, blob object들은 생략하고 commit object와 reference만 표현하겠습니다.)


새로운 commit object를 생성하지 않고 master branch의 포인터만 원래 생성되어 있던 commit object의 SHA-1값으로 변화되었습니다. 이는 merge하기 전의 dev branch가 가리키는 commit object가 master branch가 가리키는 commit object가 같기 때문입니다. 즉, master branch의 바로 다음 진행이 되어도 무방한 commit이기 때문에 단순히 branch의 포인터만 바꾼것으로 merge가 완료되었습니다. 이를 Fast forward라고 부릅니다. 그럼 아래와 같은경우는 어떨까요?


dev branch의 부모 commit이 master branch가 아니고 분기가 되어있습니다. 이런 상황에서 다시 한번 merge를 해 보겠습니다.

이번에는 fast forward가 되지 않고, 새로운 commit이 하나 생성이 되고 master branch가 해당 commit으로 옮겨갔습니다. 새로 생성된 commit은 parent를 2개를 가지고 있는것도 확인할 수 있습니다. Git의 merge는 기본적으로 이런 모델로 작동하고 있습니다.


이렇게 Git의 기본적인 구조와 branch, merge의 동작 모델까지 알아보았습니다. 위의 내용을 숙지하고 Git을 사용하면 표면적인 Git의 add, commit, merge등만 알고 사용하는 것 보다 훨씬 더 깊이 있는 이용이 가능하리라 생각합니다.

기회가 된다면 이어지는 포스팅에서는 merge, rebase의 차이를 다루고 효과적으로 Git을 사용하는 방안에 대해 다루어보겠습니다.


Reference

     - Git Internal(https://github.com/pluralsight/git-internals-pdf)

     - Pro Git(https://git-scm.com/book/)