안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

깃 (Git)

[Git] 내부 동작 원리에 대한 이해

피그브라더 2020. 1. 7. 20:28

0. 참고 도식

▲ Commit 파일, Tree 파일, Blob 파일, Index 파일, Working Directory 파일의 관계 도식 (※ 퍼가실 땐 출처 밝혀주세요!)

 

1. GIT 내부 동작 원리 이해를 위한 용어 정리

1-1. 로컬 vs 인덱스 vs 저장소

이름 실제 위치 설명
로컬 (Local)
= Working Directory
= Working Copy
프로젝트 폴더 현재 프로젝트 폴더에 존재하는 파일들 그 자체를 의미한다.
인덱스 (Index)
= Staging Area
= Cache
프로젝트 폴더 하위 .git/index 파일 개념적으로는 커밋이 이뤄질 준비가 된 파일의 내용들이 위치하는 영역을 의미하며, 실제로는 하나의 파일(.git/index)로서 존재한다. 로컬에 변동 사항이 생겼을 경우, git add 명령어를 수행하면 해당 변동 사항을 인덱스 영역에 반영시킬 수 있다. 참고로, 인덱스 파일(.git/index)에는 커밋이 이뤄질 준비가 된 파일의 내용들 각각에 대하여 그 파일명과 해당 파일의 내용을 담고 있는 Blob 파일의 주소(이름)가 기록된다.
저장소 (Repository) 프로젝트 폴더 하위 .git/objects/ 폴더 깃이 버전 관리를 하기 위해 필요로 하는 데이터들을 저장하는 곳이다. 대표적으로, 버전 관리를 시작한 시점부터 현재 시점까지 관리해온 여러 버전들에 해당하는 파일들의 내용이 Blob 파일로서 이곳에 저장되어 있다. 이곳에 저장된 파일들을 특별히 오브젝트 파일이라고 부르며, Blob 파일도 오브젝트 파일의 한 종류이다.

 

1-2. 오브젝트 파일

위에서 언급했듯이 오브젝트 파일이란 .git/objects/ 폴더에 존재하는 파일을 가리키며, 다음과 같이 분류된다.

 

종류 설명
Blob 파일 버전 관리하는 파일들 각각의 내용은 깃의 저장소에서 Blob 파일의 형태로 저장된다. 파일의 내용에 SHA1이라는 해싱 기법을 적용하여 Blob 파일의 이름을 얻어내기 때문에, 내용이 같은 파일들은 모두 하나의 Blob 파일로서 저장된다. 이러한 원리로 깃은 여러 버전에 걸쳐 존재하는 파일들의 내용을 중복 없이 관리할 수 있게 된다.
Commit 파일 하나의 버전을 생성한다는 것은 하나의 Commit 파일을 만드는 것을 의미한다. Commit 파일은 하나의 Tree 파일을 가리키게 되어 있다. 이 파일에는 가리키고 있는 Tree 파일의 주소(이름)와 직전 버전에 해당하는 Commit 파일의 주소(이름)가 기록된다.
Tree 파일 커밋 시점의 파일들 각각에 대해 그 파일명과 해당 파일의 내용을 담고 있는 Blob 파일의 주소(이름)가 기록된다. 위에서 설명했던 인덱스 파일(.git/index)과 성격이 유사하다.
Tag 파일 (본 문서에서는 설명하지 않음)

 

2. GIT 주요 명령어

2-1. git init

현재 디렉토리에 .git 폴더를 만들어서 이제부터 깃이 버전 관리를 할 수 있게끔 하는 명령어이다.

 

2-2. git add <파일명>

인덱스의 내용과 비교했을 때 로컬에서 변동된 사항을 인덱스에 반영시키는 명령어이다. 예를 들어 새로 생성된 파일, 혹은 수정/삭제된 기존 파일이 add 명령의 대상이 된다. <파일명> 대신 . 기호를 사용하면 로컬의 현재 디렉토리에서 add 명령의 대상이 되는 파일들 전부를 인덱스에 반영한다.

 

2-3. git commit -m <메시지>

인덱스의 내용을 바탕으로 새로운 버전(Commit 파일)을 생성하는 명령어이다. 즉 특정 시점에 존재하는 파일들의 정보에 대한 스냅샷을 찍어서 하나의 버전으로서 저장해두겠다는 의미이다. <메시지>에는 해당 버전에 대한 설명 등을 지정해주면 된다.

 

2-4. git status

로컬의 내용과 인덱스의 내용을 비교하여 add 명령의 대상이 되는 파일들의 목록을 표시해주고, 인덱스의 내용과 최신 커밋의 Tree 파일 내용을 비교하여 commit 명령의 대상이 되는 파일들의 목록을 표시해주는 명령어이다. 참고로 윈도우 10의 cmd에서 동작하는 GIT을 기준으로, 전자(add 대상)는 빨간 글씨로 표시되며 후자(commit 대상)는 녹색 글씨로 표시된다. 만약 로컬의 내용, 인덱스의 내용, 최신 커밋의 Tree 파일 내용이 모두 같다면 "nothing to commit"을 출력해준다.

 

더 나은 이해를 위해 예시를 하나 들어보겠다. 인덱스 파일과 Tree 파일은 특정 시점에 존재하는 파일들의 정보에 대한 스냅샷에 불과하다. 이를 기억한 채로 다음 예를 따라가며 원리를 제대로 이해해보도록 하자. 먼저, 현재 파일 3개(A, B, C)가 커밋된 직후의 상태라고 가정하자. 그렇다면 현재 로컬의 내용, 인덱스의 내용, 최신 커밋의 Tree 파일 내용은 동일할 것이다(A, B, C 3개의 파일이 존재). 이때 로컬에서 B를 삭제하고, C를 수정하고, D를 생성했다고 해보자. 그러면 상태는 다음과 같이 변화한다.

 

  • 로컬 : A, C', D
  • 인덱스 : A, B, C
  • 최신 커밋의 Tree : A, B, C

 

이때 git status 명령어를 수행하면 다음과 같이 동작한다. 먼저 로컬의 내용과 인덱스의 내용을 비교한다. 인덱스의 내용과 비교했을 때, 로컬에서 B가 사라졌고 C가 수정됐으며 D가 생성되었다는 것을 알 수 있다. 따라서 해당 변동 사항을 인덱스에 반영시킬 수 있다는 메시지를 빨간 글씨로 표시해준다. 다음으로, 인덱스의 내용과 최신 커밋 Tree의 내용을 비교한다. 그러나 둘 간에 차이가 전혀 없으므로 별다른 동작을 수행하지 않는다. 이제, git add 명령어를 수행하여 로컬의 변동 사항을 인덱스에 모두 반영시켜준다. 그러면 상태는 다음과 같이 변화한다.

 

  • 로컬 : A, C', D
  • 인덱스 : A, C', D
  • 최신 커밋 Tree : A, B, C

 

이 상태에서 다시 git status 명령어를 수행하면 어떻게 될까? 먼저 로컬의 내용과 인덱스의 내용을 비교한다. 그러나 둘 간에 차이가 전혀 없으므로 별다른 동작을 수행하지 않는다. 다음으로, 인덱스의 내용과 최신 커밋 Tree의 내용을 비교한다. 최신 커밋 Tree의 내용과 비교했을 때, 인덱스에서 B가 사라졌고 C가 수정됐으며 D가 생성되었다는 것을 알 수 있다. 따라서 해당 변동 사항을 커밋할 수 있다는 메시지를 녹색 글씨로 표시해준다. 이제 git commit 명령어를 수행하면 인덱스의 내용으로 새로운 Commit 파일이 생성되면서 커밋이 마무리될 것이다. 그러나 공부를 위해 이 상황에서 git commit 명령어를 수행하지 않고 다시 로컬의 D를 수정했다고 해보자. 그러면 상태는 다음과 같이 변화한다.

 

  • 로컬 : A, C', D'
  • 인덱스 : A, C', D
  • 최신 커밋 Tree : A, B, C

 

난장판이 되었다. 세 영역의 내용이 모두 달라졌기 때문이다. 이때 git status 명령어를 수행하면 어떻게 될까? 먼저, 인덱스의 내용과 비교했을 때 로컬에서 D가 수정되었다는 것을 알 수 있으므로 이를 인덱스에 반영시켜줄 수 있다는 메시지를 빨간 글씨로 표시해준다. 다음으로, 최신 커밋 Tree의 내용과 비교했을 때 인덱스에서 B가 사라졌고 C가 수정됐으며 D가 생성되었다는 것을 알 수 있으므로 이를 커밋할 수 있다는 메시지를 녹색 글씨로 표시해준다.

 

3. GIT 브랜치

3-1. HEAD와 브랜치 포인터

명칭 실제 위치 설명
HEAD .git/HEAD 현재 체크아웃되어 있는 브랜치의 포인터를 가리키는 포인터이다. 브랜치의 포인터가 아닌 일반적인 Commit 파일을 가리키게 할 수도 있다(이러한 경우를 detached HEAD라고 표현한다).
브랜치 포인터 .git/refs/heads/{브랜치명} 해당 브랜치의 가장 최신 Commit 파일을 가리키는 포인터이다.

 

3-2. git reset & git checkout

명령어 설명
git reset 브랜치 포인터를 지정된 Commit 파일을 가리키도록 바꾼다.

--soft 옵션 : 브랜치 포인터만 바꾼다. (로컬 및 인덱스 유지)
--mixed 옵션 : 브랜치 포인터뿐 아니라 인덱스까지 바꾼다. (로컬 유지)
--hard 옵션 : 브랜치 포인터뿐 아니라 인덱스와 로컬까지 바꾼다.
git checkout HEAD를 지정된 브랜치의 포인터를 가리키도록 바꾼다.

 

3-3. git merge

명령어 설명
git merge {브랜치명} 해당 브랜치 내용을 가져와 현재 체크아웃되어 있는 브랜치와 병합시켜 새로운 Commit 파일을 생성하고, 현재 브랜치의 포인터를 새로운 Commit 파일을 가리키도록 바꾼다.

※ 단, 공통조상 Commit 파일과 현재 브랜치의 Commit 파일이 완전히 같은 경우에는 Commit 파일이 새로 생성되지 않고(새로 생성되게 옵션을 줄 수는 있음) 현재 브랜치의 포인터만 옮겨주는 Fast-Forward 병합이 이루어진다.

충돌이 발생할 경우, 대략 다음과 같은 로직으로 깃이 동작한다. (뇌피셜)

1) 병합이 성공적으로 이뤄진 파일들은 인덱스와 로컬에 그대로 반영해준다.
2) 병합에 실패한 파일들의 경우 인덱스에 올리지 않고, 로컬에 특수 문구를 삽입한 채로 반영하여 사용자로 하여금 충돌이 발생한 파일을 찾아가 충돌을 해결한 뒤 add/commit 명령을 통해 정상적으로 새로운 Commit 파일을 생성할 수 있게끔 한다.

 

3-4. HEAD와 브랜치 포인터의 히스토리

실제 위치 설명
ORIG_HEAD HEAD 값의 변화를 기록한 것으로, 위험한 명령(EX. git reset)을 수행하기 직전의 커밋을 조사하기에 유용하다.
.git/logs/refs/heads/{브랜치명} 해당 브랜치의 포인터가 어떤 명령에 의해 어떻게 바뀌었는지를 기록한 파일

 

4. 원격 저장소 GitHub

4-1. 원격 저장소

이름에서 의미하듯이 Working Directory와 Index가 없고 Repository만 존재하는 원격 서버라고 이해하면 된다. GitHub에서 제공하는 Repository가 대표적인 원격 저장소이다. 원격 저장소를 활용하면 공동 개발 시 팀원들과의 효율적인 협업이 가능해진다.

 

만약 원격 저장소의 역할을 하는 저장소를 로컬에서 만들고 싶다면, --bare 옵션을 주어 git init 명령을 수행하면 된다.

 

원격 저장소를 대상으로 명령어를 수행할 때마다 권한 때문에 매번 로그인을 요구하는데, HTTPS가 아닌 SSH 통신 방법을 이용하면 자동 로그인이 가능하다. ssh-keygen 명령어로 공개키와 개인키를 만든 뒤, 개인키는 자신의 PC에 잘 보관하고 공개키를 자신의 GitHub에 등록하면 된다.

 

4-2. git remote

명령어 설명
git remote add {원격 저장소 별칭} {주소} 로컬과 해당 원격 저장소를 연결한다. 원격 저장소 별칭으로는 보통 origin을 많이 사용한다.
git remote -v 로컬과 연결된 원격 저장소들의 정보를 보여준다.

 

4-3. git push/fetch/pull

명령어 설명
git push {원격 저장소 별칭} {브랜치명} 지정된 원격 저장소에 존재하는 해당 원격 브랜치에 현재 로컬 브랜치의 내용을 push 한다.

--set-upstream 혹은 -u 옵션을 주면 현재 로컬 브랜치와 해당 원격 브랜치가 연결된다. 이를 보고 '현재 로컬 브랜치의 업스트림을 지정한다.'라고 표현한다. 이렇게 업스트림이 지정된 상태에서 인자 없이 git push/fetch/pull 명령을 수행하면, 업스트림으로 지정되어 있는 원격 브랜치를 대상으로 해당 명령이 수행된다.

참고로 git clone으로 원격 저장소의 내용을 가져온 경우, 각각의 로컬 브랜치들에 대하여 같은 이름의 원격 브랜치가 업스트림으로 자동 지정된다.
git fetch {원격 저장소 별칭} {브랜치명} 지정된 원격 저장소에 존재하는 해당 원격 브랜치의 내용을 ./git/refs/remote/{원격 저장소 별칭}/{브랜치명}라는 이름으로 로컬에 가져온다. 인자를 명시하지 않으면 현재 로컬 브랜치의 업스트림으로 지정된 원격 브랜치를 가져오게 된다.
git pull {원격 저장소 별칭} {브랜치명} 
= git fetch {원격 저장소 별칭} {브랜치명} + 
   git merge {원격 저장소 별칭}/{브랜치명}
먼저 원격 브랜치의 내용을 가져온 뒤(= git fetch), 가져온 원격 브랜치의 내용을 현재 로컬 브랜치로 가져와 병합(= git merge)을 진행한다.

 

4-4. 원격 브랜치 포인터

실제 위치 설명
.git/refs/remote/{원격 저장소 별칭}/{브랜치명} 지정된 원격 저장소에 존재하는 해당 원격 브랜치의 포인터를 나타낸다. 당연히 실시간으로 갱신되진 않고, git push/fetch/pull 명령을 수행했을 때만 원격 브랜치 정보로 갱신된다.