[Git/GitHub] Git/GitHub 버전 관리 기본 개념

dEpayse
dEpayse_publication
11 min readFeb 19, 2022

지난 포스트에서 Git/GitHub가 무엇이고, 어떻게 설치하고 시작하는 지에 대해 다루었다. 이번 포스트에서는 Git/GitHub이 어떻게 버전 관리를 하는지, Git/GitHub에 대해서 더 잘 이해하고 사용하기 위해 필요한 개념들을 다룬다.

아래는 지난 포스트의 링크이다.

Git 파일의 영역과 상태

Git은 어떤 방식으로 버전 정보를 저장할까?

지난 포스트에서 git으로 프로젝트의 버전을 관리하려면, ‘git init’ 이라는 명령어로 프로젝트의 루트 폴더에 git을 심어줘야 한다고 했던 사실을 기억해보자. 이 명령이 성공하면 프로젝트 루트 폴더에 ‘.git’이라는 숨겨진 폴더가 생성되는 것을 확인했었다. 우리가 파일의 내용을 수정하고 단순히 ctrl+s로 저장하면 당연히 우리의 pc에서는 저장이 되지만, git으로 버전 관리가 된 것은 아니었다. commit이라는 것을 생성해야 하나의 버전이 생성되는 것이다. 그러나 commit을 하려면, 지난 포스트에서 봤듯이 ‘git commit’ 명령어 전에 ‘git add’ 명령어를 사용해주어야 한다. 바로 이 ‘git add’ 명령어가 변경된 파일을 ‘.git’폴더에 정보를 추가해주는 명령어인 것이다. 주의할 점은 ‘.git’ 폴더에 정보가 추가가 되는 것은 완전히 같은 파일 자체가 내부에 생성되어 저장되는 것은 아니다. ‘.git’에 add된 파일은 스테이지 영역에 들어가게 되며, ‘staged 되었다.’고 표현한다. Git의 동작 방식을 이해하려면, Git의 관점에서의 영역과, 파일의 네 가지 상태를 이해하는 것이 좋다.

Git 관점에서의 영역

Git의 관점에서 로컬 pc의 영역은 Fig1과 같이 구분할 수 있다.

Fig1. 로컬 pc에서 git의 영역
  • Working Directory : 우리가 버전을 관리하는 프로그램의 프로젝트 폴더를 의미하는 공간
  • Git Local Repository : Working Directory 내에 git이 초기화되면 존재하는 git의 로컬 저장소
  • Staging Area : 다음 커밋에 어떤 새로운 파일을 추가할 것인지, 또 변경이 있는 파일 중 어떤 파일을 다음 커밋에 포함시킬 것인지를 결정하기 위한 영역. ‘.git’ 폴더 내부에 ‘index’ 라는 파일로서 존재한다. 원격 저장소에는 Staging Area가 없다.

Git 관점에서 파일의 상태

또 git의 관점에서 git 영역에 있는 파일은 Fig2와 같이 네 가지 상태로 분류할 수 있다.

  • Untracked : git 으로 추적이 불가능한 상태의 파일
  • Unmodified : git 으로 추적이 가능하며 전 커밋과 비교하였을 때 수정되지 않은 상태의 파일
  • Modified : git 으로 추적이 가능하며 전 커밋과 비교하였을 때 수정된 상태의 파일
  • Staged : git 으로 추적이 가능하며 전 커밋과 비교하였을 때 새로 생긴 파일이나, 전 커밋과 비교하였을 때 수정된 파일 중 다음 커밋에 포함시킬 파일
Fig2. 로컬 pc에서 git 파일의 4가지 상태

Git 객체의 4가지 요소

Git은 git 로컬 저장소(.git 폴더) 내부에 object 폴더를 두어 4가지 형태의 파일로 버전을 관리한다. object 폴더에 존재하는 네 가지 형태의 파일 이름은 SHA-1 해싱 기법을 사용하여 만들고(40자의 16진수), object 폴더 내의 하위 폴더는 파일 시스템의 성능을 위해 40자 중 앞의 2글자로 구분되며, 하위 폴더 내부로 들어가면 40자 중 2글자를 제외한 38자의 이름을 갖는 파일들이 존재한다.

  • Blob : 관리할 파일의 내용을 담고 있는 객체. 파일명과 같은 파일의 메타데이터를 제외한 오로지 파일 내용이 Blob 파일의 형태로 저장된다. 내용이 같은 파일들은 모두 하나의 Blob 파일로 간주된다. Blob은 Binary Large Object의 줄임말이다.
  • Tree : Git에서도 폴더 구조를 관리할 수 있게 해주는 객체. 디렉토리를 표현하기 위해 Blob과 Tree 형태 두 가지 형태의 파일 정보를 포함할 수 있다. 갖고 있는 정보의 종류로는 파일 식별자, blob인지 tree인지에 대한 정보, object hash key(id), 파일명과 확장자가 있다. 파일 식별자는 100644(읽기 파일(blob)), 100755(실행 파일(blob)), 040000(디렉토리(tree)) 등이 있다.
  • Commit : 관리할 버전 중 하나를 의미하는 객체. 커밋이 가리키는 tree의 ID, 바로 전 커밋의 Id, 커밋한 사람(committer)의 정보, 작성자(author)의 정보, 커밋 메세지를 담고 있다. 하나의 Commit 파일은 하나의 Tree 파일을 가리키는데, 이를 통해 해당 버전의 모든 파일들을 git의 디렉토리인 트리로 저장하여 관리할 수 있다. 커밋은 바로 전 커밋의 정보를 parent 키에 값으로 저장함으로써 커밋 이력들을 이어서 저장할 수 있다.
  • Annotated Tag : 커밋을 참조하기 쉽도록 태그명 등의 정보를 담고 있는 객체. 태그명, 태그한 사람의 정보, PGP 서명 정보를 포함한다. Git에서 태그는 일반 태그(Lightweight Tag)와 주석 태그(Annotated Tag) 두 종류로 나뉘는데, Annotated Tag 만 ‘.git/object/’ 에 저장된다. Lightweight tag는 태그의 이름만 ‘.git/refs/tag’에 저장하고, Annotated tag는 그 외의 정보들을 저장할 수 있다.
Fig3. git objects example

Fig3은 git 의 4가지 종류의 객체의 예시를 나타낸다. 객체의 체크섬을 나타내는 해시값들은 40자 전부가 아닌 일부를 표시하였다.

Branch, HEAD

Branch란?

Branch의 개념은 하나의 Git Repository 안에서 사용할 수 있는 서로 다른 작업 공간이다. 따라서 branch를 변경하면 로컬 pc의 Working Directory의 파일들도 변경된다.

(내부적으로는 커밋가리키는 포인터이다. 내부 동작의 상세한 내용은 잠시 후 “‘.git’ 폴더 내의 branch” 파트에서 다루고, 우선 Branch의 개념에 대해서 더 알아보자.)

GitHub을 사용하여 협업하려고 할 때, 같은 소스 코드로 작성된 파일을 동시에 두 명의 개발자가 수정하는 상황을 생각해보자. 각 개발자는 독립적인 개발을 할 수 있어야 한다. 하지만 다른 로컬 pc 라고 하더라도 같은 branch에서 작업한다면 같은 원격 저장소에 push할 때마다 코드에 다른 점이 생길 수 있고(‘충돌’이라고 표현한다.), 이때마다 충돌을 해결해주어야 한다. 이는 개발하는 데 많은 시간 비용을 발생시킨다. Fig4–1은 A, B 개발자가 같은 branch에서 같은 파일을 수정하고 서로 push했을 때 발생할 수 있는 문제이다.

Fig4–1. 협업 시 같은 branch 를 사용할 때 생기는 문제

Git은 효율적으로 개발할 수 있도록 작업 공간을 분리하는 기능을 제공하는데, 그것이 바로 branch 이다. Fig4–1 과 같은 상황에서 branch를 이용하면 Fig4–2와 같이 작업이 가능하다. (Fig 4–1Fig4–2에서 branch를 선으로 표현했지만, 내부적으로는 조금 다르다. 다음 파트에서 더 알아보자.)

Fig4–2. 서로 다른 branch 를 사용하여 협업하기

A, B 개발자는 각각 같은 소스 코드를 가져와서 서로 다른 브랜치를 사용하여 작업하고 push하면서 진행할 수 있다. Fig4–2에서는 A, B 개발자가 각각 한 번의 push만을 진행했지만, 여러 번의 push를 해도 다른 branch이기 때문에 Fig4–1처럼 충돌이 발생하지 않는다. 그러나 그림의 4번 과정에서 역시 충돌이 생긴다. 같은 파일의 서로 다른 코드가 하나의 branch로 합쳐지기 때문이다. 여기서 하나의 branch로 합치는데 사용하는 방법으로, pull request(줄여서 pr이라고 부르기도 한다.)라는 과정을 통해 서로 어떤 충돌이 발생했는지 github에서 확인하여 하나의 master 브랜치로 병합할 수 있다. Fig4–2에서 5번 과정인 pull은 push와 반대로 원격 저장소에서 로컬 저장소로 정보를 가져오는 명령이다.

‘.git’ 폴더 내의 branch

branch는 ‘.git’ 폴더의 refs 폴더에 존재한다. ‘Git 객체의 4가지 요소’ 파트의 커밋에서 봤듯이 커밋은 그 전 커밋의 정보를 부모 커밋의 id로 저장함으로써 이력을 기록한다. 또 바로 위 파트에서 봤듯이 branch는 내부적으로 커밋을 가리키는 포인터이다. 두 가지 정보를 조합하면 Fig5–1와 같은 도식을 그려볼 수 있다.

Fig5–1. branch의 개념1

Fig5–1을 정리하면

  • 커밋은 부모 커밋의 id 정보를 갖고있다.(부모 커밋을 가리킨다.)
  • branch커밋을 가리키는 포인터이다.
  • branch_a 가 가리키는 커밋은 branch_a 의 ‘branch tip(브랜치 팁)’이라고 한다.

그렇다면 새로운 branch를 생성하면 어떻게 될까? 단지 같은 커밋을 가리키는 커밋이 하나 더 생기는 것 뿐이다. branch는 그저 커밋을 가리키기만 하고, 커밋 각각이 그 전 커밋의 정보를 갖고 있기 때문에 그 이력들을 모두 알 수 있다. Fig5–2은 새로운 브랜치인 branch_b를 만들고 branch_a, branch_b에서 각각 하나의 커밋을 추가했을 때 상황의 도식이다.

Fig5–2. branch의 개념2

Fig5–2를 정리하면

  • branch_a의 이력은 4개의 커밋이다.
  • branch_b의 이력은 4개의 커밋이다.
  • 3개의 커밋은 branch_a, branch_b 두 개의 branch 모두 포함하지만 git repository에는 브랜치 별로 각각 존재하는 것이 아니다. 각각의 커밋이 그 전 커밋을 가리키고, 브랜치는 커밋을 가리키는 포인터일 뿐이다.

HEAD란?

HEAD는 현재 브랜치를 가리키는 포인터이다. 즉 내가 어떤 작업 공간에 있는지 가리킨다. HEAD는 ‘.git’ 폴더의 루트 디렉토리에 존재한다.

HEAD가 branch처럼 직접 커밋을 가리킬 수 있지만, 권장되진 않는다.

  • (이런 경우를 detached Head라고 한다. 테스트가 목적이라면 detached HEAD를 이용하기 보다 test branch를 만들어 사용하는 것을 권장한다.)
  • (이전 커밋에서 새로운 branch를 생성하고 싶을 때 detached HEAD를 사용할 수 있다.)
Fig6. HEAD 개념

Fig6은 현재 branch_a에 있을 때의 상황이다. HEAD가 branch_a를 가리키는 것을 볼 수 있다.

이번 포스트에서는 Git 파일의 영역과 상태, Git의 4가지 객체, Branch 와 HEAD 라는 Git에서 가장 중요한 개념들을 다뤄보았다. 이 중요한 개념들을 잘 익히면 Git을 이해하고 사용하는데 도움이 될 것이라고 생각한다.

Reference

  1. [Git official — Book] — https://git-scm.com/book/en/v2
  2. [IT 엘도라도] “내부 동작 원리에 대한 이해” — https://it-eldorado.tistory.com/4
  3. [아프니까 개발자다] “깃의 속사정, 4대 원소를 파헤치기” — https://storycompiler.tistory.com/7
  4. [Tecoble] “.git 내부 구조 파헤치기” — https://tecoble.techcourse.co.kr/post/2021-07-08-dot-git/
  5. [Stackoverflow] “What is a ‘branch tip’ in Git?” — https://stackoverflow.com/questions/16080342/what-is-a-branch-tip-in-git

--

--

dEpayse
dEpayse_publication

나뿐만 아니라 다른 사람들도 이해할 수 있도록 작성하는, 친절한 블로그를 목표로.