Git
Chapters ▾ 2nd Edition

10.6 Git의 내부 - 데이터 전송 프로토콜

데이터 전송 프로토콜

Git에서 데이터를 전송할 때 보통 두 가지 종류의 프로토콜을 사용한다. 하나는 “dumb” 프로토콜이고 다른 종류는 “스마트” 프로토콜이다. 두 종류 프로토콜을 통해 Git이 어떻게 데이터를 전송하는지 살펴본다.

Dumb 프로토콜

읽기전용으로만 사용하는 HTTP 저장소를 Clone 하거나 Fetch 할 때가 Dumb 프로토콜을 사용하는 때이다. Dumb 프로토콜이라 부르는 이유는 서버가 데이터를 전송할 때 Git에 최적화된 어떤 작업도 전혀 사용하지 않기 때문이다. 단지 Fetch 과정은 HTTP GET 요청을 여러 번 보낼 뿐이다. 이때 클라이언트는 서버의 Git 저장소 레이아웃이 특별하지 않다고 가정한다.

Note

요즘은 Dumb 프로토콜을 사용하는 경우가 드물다. Dumb 프로토콜을 사용하면 데이터 전송을 비밀스럽게 하기 어려워서 비공개용 저장소의 데이터를 전송하기에 적합하지 않다. 이후에 설명할 스마트 프로토콜을 사용하도록 조언하는 바이다.

simplegit 라이브러리에 대한 http-fetch 과정을 살펴보자.

$ git clone http://server/simplegit-progit.git

우선 info/refs 파일을 내려받는다. 이 파일은 update-server-info 명령으로 작성되기 때문에 post-receive 훅에서 update-server-info 명령을 호출해줘야만 HTTP를 사용할 수 있다.

=> GET info/refs
ca82a6dff817ec66f44342007202690a93763949     refs/heads/master

리모트 Refs와 SHA-1 값이 든 목록을 가져왔고 다음은 HEAD Refs를 찾는다. 이 HEAD Refs 덕택에 데이터를 내려받고 나서 어떤 Refs를 Checkout 할 지 알게 된다.

=> GET HEAD
ref: refs/heads/master

데이터 전송을 마치면 master 브랜치를 Checkout 해야 한다. 지금은 아직 전송을 시작하는 시점이다. info/refsca82a6 커밋에서 시작해야 한다고 나와 있다. 그래서 그 커밋을 기점으로 Fetch 한다.

=> GET objects/ca/82a6dff817ec66f44342007202690a93763949
(179 bytes of binary data)

서버에 Loose 포맷으로 돼 있기 때문에 HTTP 서버에서 정적 파일을 가져오듯이 개체를 가져오면 된다. 이렇게 서버로부터 얻어온 개체를 zlib로 압축을 풀고 Header를 떼어 내면 아래와 같은 모습이 된다.

$ git cat-file -p ca82a6dff817ec66f44342007202690a93763949
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
author Scott Chacon <schacon@gmail.com> 1205815931 -0700
committer Scott Chacon <schacon@gmail.com> 1240030591 -0700

changed the version number

아직 개체를 두 개 더 내려받아야 한다. cfda3b 개체는 방금 내려받은 커밋의 Tree 개체이고, 085bb3 개체는 부모 커밋 개체이다.

=> GET objects/08/5bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
(179 bytes of data)

커밋 개체는 내려받았다. 하지만, Tree 개체를 내려받으려고 하니 아래와 같은 오류가 발생한다.

=> GET objects/cf/da3bf379e4f8dba8717dee55aab78aef7f4daf
(404 - Not Found)

이런! 존재하지 않는다는 404 메시지가 뜬다. 해당 Tree 개체가 서버에 Loose 포맷으로 저장돼 있지 않을 수 있다. 해당 개체가 다른 저장소에 있거나 저장소의 Packfile 속에 들어 있을 때 그렇다. 우선 Git은 다른 저장소 목록에서 찾는다.

=> GET objects/info/http-alternates
(empty file)

다른 저장소 목록에 없으면 Git은 Packfile에서 해당 개체를 찾는다. 이렇게 하면 프로젝트를 Fork 해도 디스크 공간을 효율적으로 사용할 수 있다. 우선 서버에서 받은 다른 저장소 목록에는 없어서 개체는 확실히 Packfile 속에 있다. 어떤 Packfile이 있는지는 objects/info/packs 파일에 들어 있다. 이 파일도 update-server-info 명령이 생성한다.

=> GET objects/info/packs
P pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack

서버에는 Packfile이 하나 있다. 개체는 이 파일 속에 있다. 이 개체가 있는지 Packfile의 Index(Packfile이 포함하는 파일의 목록)에서 찾는다. 서버에 Packfile이 여러 개 있으면 이런 식으로 개체가 어떤 Packfile에 있는지 찾는다.

=> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.idx
(4k of binary data)

이제 Packfile의 Index를 가져와서 개체가 있는지 확인한다. Packfile Index에서 해당 개체의 SHA-1 값과 오프셋을 파악한다. 개체를 찾았으면 해당 Packfile을 내려받는다.

=> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack
(13k of binary data)

Tree 개체를 얻어 오고 나면 커밋 데이터를 가져 온다. 아마도 방금 내려받은 Packfile 속에 모든 커밋 데이터가 들어 있을 것이다. 서버에 더는 전송 요청을 보내지 않는다. 다 끝나면 Git은 HEAD가 가리키는 master 브랜치의 소스코드를 복원해놓는다.

스마트 프로토콜

Dumb 프로토콜은 매우 단순하다는 장점이 있으나 데이터를 효율적으로 전송할 수 없다. 스마트 프로토콜로 데이터를 전송하는 것이 더 일반적이다. 이 프로토콜은 리모트 서버에서 처리해야 할 작업이 있다. 서버는 클라이언트가 어떤 데이터를 갖고 있고 어떤 데이터가 필요한지 분석하여 실제로 전송할 데이터를 추려낸다. 서버가 할 일을 두 가지 일로 구분할 수 있는데 데이터를 업로드할 때 하는 일과 다운로드할 때 하는 일이 다르다.

데이터 업로드

리모트 서버로 데이터를 업로드하는 과정은 send-packreceive-pack 과정으로 나눌 수 있다. 클라이언트에서 실행되는 send-pack 과 서버의 receive-pack 은 서로 연결된다.

SSH

origin URL이 SSH URL인 상태에서 git push origin master 명령을 실행하면 Git은 send-pack 을 시작한다. 이 과정에서는 SSH 연결을 만들고 이 SSH 연결을 통해서 아래와 같은 명령어를 실행한다.

$ ssh -x git@server "git-receive-pack 'simplegit-progit.git'"
00a5ca82a6dff817ec66f4437202690a93763949 refs/heads/master□report-status \
	delete-refs side-band-64k quiet ofs-delta \
	agent=git/2:2.1.1+github-607-gfba4028 delete-refs
0000

git-receive-pack 명령은 Refs 정보를 한 라인에 하나씩 보여준다. 첫 번째 라인에는 master 브랜치의 이름과 SHA-1 체크섬을 보여주는데 여기에 서버의 Capability도 함께 보여준다(여기서는 report-status, delete-refs, 기타 등등과 클라이언트 Identifier를 표시한다).

각 라인의 처음은 4 바이트는 뒤에 이어지는 나머지 데이터의 길이를 나타낸다. 첫 라인을 보자. 00a5로 시작하는데 10진수로 165를 나타낸다. 첫 줄의 처음 4 바이트를 제외한 나머지 길이가 165 바이트라는 뜻이다. 마지막 라인은 값은 0000이다. 이는 서버가 Refs 목록의 출력을 끝냈다는 것을 의미한다.

서버에 뭐가 있는지 알기 때문에 이제 서버에 없는 커밋이 무엇인지 알 수 있다. Push 할 Refs에 대한 정보는 send-pack 과정에서 서버의 receive-pack 과정으로 전달된다. 예를 들어 master 브랜치를 업데이트하고 experiment 브랜치를 추가할 때는 아래와 같은 정보를 서버에 보낸다.

0076ca82a6dff817ec66f44342007202690a93763949 15027957951b64cf874c3557a0f3547bd83b3ff6 \
	refs/heads/master report-status
006c0000000000000000000000000000000000000000 cdfdb42577e2506715f8cfeacdbabc092bf63e8d \
	refs/heads/experiment
0000

Git은 예전 SHA-1, 새 SHA-1, Refs 이름을 한 줄 한 줄에 담아 전송한다. 첫 라인에는 클라이언트 Capability도 포함된다. SHA-1 값이 모두 '0’인 것은 없음(無)을 의미한다. experiment Refs는 새로 추가하는 것이라서 왼쪽 SHA-1값이 모두 0이다. 반대로 오른쪽 SHA-1 값이 모두 '0’이면 Refs를 삭제한다는 의미다.

그다음에 서버에 없는 객체를 전부 하나의 Packfile에 담아 전송한다. 마지막에 서버는 성공했거나 실패했다고 응답한다.

000eunpack ok
HTTP(S)

HTTP를 통해 데이터를 업로드하는 과정도 크게 다르지 않지만 처음 핸드쉐이킹 과정만 약간 다르다. 우선 아래와 같은 요청으로 시작한다.

=> GET http://server/simplegit-progit.git/info/refs?service=git-receive-pack
001f# service=git-receive-pack
00ab6c5f0e45abd7832bf23074a333f739977c9e8188 refs/heads/master□report-status \
    delete-refs side-band-64k quiet ofs-delta \
    agent=git/2:2.1.1~vmg-bitmaps-bugaloo-608-g116744e
0000

첫 번째 클라이언트 요청과 서버의 응답이다. 이어지는 클라이언트 요청은 POST 메소드를 써서 send-pack 명령이 제공하는 데이터를 서버로 전송하는 요청이다.

=> POST http://server/simplegit-progit.git/git-receive-pack

POST 요청은 send-pack 의 결과와 Packfile을 데이터로 전송한다. 전송한 데이터가 서버에서 처리된 결과가 HTTP 응답으로 전달된다.

데이터 다운로드

데이터를 다운로드하는 것는 fetch-packupload-pack 과정으로 나뉜다. 클라이언트가 fetch-pack 을 시작하면 서버의 upload-pack 에 연결되고 서로 어떤 데이터를 내려받을지 결정한다.

SSH

SSH 프로토콜을 사용하면 fetch-pack 은 아래와 같이 실행한다.

$ ssh -x git@server "git-upload-pack 'simplegit-progit.git'"

fetch-pack 과 연결된 upload-pack 은 아래와 같은 데이터를 전송한다.

00dfca82a6dff817ec66f44342007202690a93763949 HEAD□multi_ack thin-pack \
	side-band side-band-64k ofs-delta shallow no-progress include-tag \
	multi_ack_detailed symref=HEAD:refs/heads/master \
	agent=git/2:2.1.1+github-607-gfba4028
003fe2409a098dc3e53539a9028a94b6224db9d6a6b6 refs/heads/master
0000

receive-pack 의 응답과 매우 비슷하지만, Capability 부분은 다르다. HEAD Refs(symref=HEAD:refs/heads/master)도 알려주기 때문에 저장소를 Clone 하면 무엇을 Checkout 해야 할지 안다.

fetch-pack 은 이 정보를 살펴보고 이미 가지는 개체에는 “have” 를 붙이고 내려받아야 하는 개체는 “want” 를 붙인 정보를 만든다. 마지막 라인에 “done” 이라고 적어서 보내면 서버의 upload-pack 은 해당 데이터를 Packfile로 만들어 전송한다.

003cwant ca82a6dff817ec66f44342007202690a93763949 ofs-delta
0032have 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
0009done
0000
HTTP(S)

HTTP로 Fetch 하는 과정은 두 개의 HTTP 요청으로 이루어진다. 첫 번째 요청은 GET 요청으로 응답 결과는 SSH에서 본 내용과 같다.

=> GET $GIT_URL/info/refs?service=git-upload-pack
001e# service=git-upload-pack
00e7ca82a6dff817ec66f44342007202690a93763949 HEAD□multi_ack thin-pack \
	side-band side-band-64k ofs-delta shallow no-progress include-tag \
	multi_ack_detailed no-done symref=HEAD:refs/heads/master \
	agent=git/2:2.1.1+github-607-gfba4028
003fca82a6dff817ec66f44342007202690a93763949 refs/heads/master
0000

이 결과는 SSH 연결을 사용할 때 git-upload-pack 명령을 실행한 것과 비슷하지만 이어지는 두 번째 요청이 다르다.

=> POST $GIT_URL/git-upload-pack HTTP/1.0
0032want 0a53e9ddeaddad63ad106860237bbf53411d11a7
0032have 441b40d833fdfa93eb2908e52742248faf0ee993
0000

전송할 내용은 앞에서 살펴본 것과 같다. 전송한 데이터를 서버에서 처리된 결과가 HTTP 응답으로 전달되고 결과에 따라 Packfile이 포함되어 있을 수 있다.

프로토콜 요약

이번 절을 통해 Git이 사용하는 데이터 전송 프로토콜을 간단하게 살펴보았다. Git이 사용하는 데이터 전송 프로토콜에는 multi_ackside-band 같은 추가적인 많은 기능도 포함하고 있지만, 이 책에서 다룰 수 없어 설명하지는 않는다. 이 책의 내용은 Git이 어떻게 클라이언트와 서버 간에 데이터를 주고받는지 기본적인 느낌을 전달하기 위해 노력한다. 데이터 전송 프로토콜의 많은 기능을 활용해보고 싶다면 Git 소스코드를 살펴보는 것이 좋다.