git, 발을 들어라

redjen·2024년 1월 28일
8

월간 딥다이브

목록 보기
1/11

git

개발자 간 협업의 중요성과, git을 사용하는 이유를 간단히 ChatGPT에게 물어보았다.

개발자 간 협업은 효율적인 소프트웨어 개발을 위해 중요하며, 버전 관리 시스템인 git은 여러 개발자가 동시에 작업할 때 코드 충돌을 방지하고 작업 내역을 체계적으로 관리하는 데 도움을 줍니다.

Git은 분산 버전 관리, 가볍고 빠른 브랜치 관리, 그리고 다양한 협업 플랫폼과의 통합으로 인해 현대 소프트웨어 개발에서 널리 사용되고 있습니다.

ChatGPT가 잘 요약해 주었듯이, git은 개발 과정에서의 편의성과 협업을 위해 가장 많이 사용되는 소프트웨어이다.

그런데 나는 과연 지금까지 git이 어떻게 작동하는지 이해하고 사용하고 있는지? 에 대한 의문이 들어, 우리가 git addgit commit, git push 할 때 어떤 일들이 발생하는지 간략히 이해해보는 시간을 가졌다.

git object에 대한 이해

https://git-scm.com/book/en/v2/Git-Internals-Git-Objects

원 매뉴얼이 정말 잘 작성되어 있다! 보다 정확한 이해를 위해서는 직접 읽고 따라 해보는 것을 강력하게 추천한다.

git은 컨텐츠 주소 지정이 가능한 파일 시스템이다. Git의 핵심은 단순한 key - value 데이터 저장소이다.

git hash-object 커맨드

git hash-object 명령어는 다음의 역할을 수행한다.

  • .git/objects 디렉토리 내부에 저장된 데이터를 들여다 본다.
  • 이후에 해당 데이터 객체에 상응하는 유니크한 키 값을 리턴해준다.

해당 명령어를 사용해서 git 시스템 상에 새로운 key - value 데이터를 생성할 수도 있다.

echo 'test!' | git hash-object -w --stdin

임의의 git repository (git init을 마친) 에서 수행했을 때에는 다음이 리턴되었다.
989f51ae9a59308340a3827f0df9de24033f97b6

해당 값은 git 시스템이 git 데이터베이스에 저장될지도 모르는 유니크한 키 값이다. 즉

  • 해당 키 값은 로컬에 우선 저장되며
  • 해당 키 값을 통해 방금 생성한 데이터 객체를 참조 가능하다.
  • 해당 키 값은 40자의 문자열로 이루어져 있다.
    • 이는 저장한 문자열에 헤더를 더한 컨텐츠의 체크섬 해시 (SHA-1) 값이다. (본 글의 하단에서 어떻게 이 값을 생성하는 지를 간략하게 소개한다)

git cat-file 커맨드

cat 명령어는 파일 이름을 받아서 그 내용을 콘솔에 쫙 뿌려주는 명령어이다.

비슷하게, git cat-file 커맨드는 위에서 생성한 객체 데이터의 키 값을 인자로 받아 해당 객체를 볼 수 있는 여러 유용한 도구를 제공한다. (매뉴얼에서는 스위스제 군용 나이프에 비유한 것이 인상 깊다)

git cat-file -p 989f51ae9a59308340a3827f0df9de24033f97b6
test!
echo 'version 1' > test.txt
git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30
echo 'version 2' > test.txt
it hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
cat test.txt
version 2
git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob
  • git hash-object 커맨드를 통해서 git 시스템 상에 추가된 데이터 객체들은 ./git/objects 폴더 내에 저장된다.
    • 위 예시에서 볼 수 있었듯, 동일한 파일에 저장된 데이터가 서로 다르다면 key 값이 다르게 생성되어 저장된다.
  • git cat-file-t 옵션을 주게 되면 (git cat-file -t <object key>) 해당 키 값에 상응하는 데이터 객체의 타입을 조회할 수 있다.
    • 위 예시에서 추가했던 단순 텍스트 데이터는 blob 타입으로 추가된 것을 확인할 수 있었다.

blob 타입 이외에도 git 시스템에서 지원하는 다른 객체 타입을 알아보자.

Tree Objects

직감적으로 느꼈을 수도 있겠지만, 위에서 살펴보았던 blob 타입으로만으로는 파일 이름과 파일들을 함께 아우르는 폴더를 저장하지 못한다.

이런 문제를 해결해주는 것이 tree 객체 타입이다.

  • git은 유닉스 파일 시스템에 파일을 저장하는 것과 유사하지만 훨씬 간단한 방식으로 데이터를 저장한다.
  • 모든 컨텐츠들은 treeblob 객체로써 저장된다.
    • 유닉스에서 디렉토리 진입점 <-> tree
    • 유닉스에서의 inode 또는 파일 컨텐츠 <-> blob
  • 즉 단일 tree 객체는 하나 이상의 진입점을 포함한다.
    • 각 진입점은 연결된 '모드, 타입, 파일 이름' 또는 하위 트리의 SHA-1 해시 값을 아우른다.
> 즉 git 상에 저장되는 데이터는 컨셉 상 위와 같이 표현될 수 있다.
  • README, Rakefile은 파일로써, blob 타입 객체로 저장되며 tree 객체에 의해 참조된다.
  • lib 폴더 내부에 저장된 simplegit.rb 파일은 blob 타입 객체로 저장된다.
  • lib 폴더는 tree 객체로써, simplegit.rb 파일을 참조한다.
  • lib 폴더에 상응하는 tree 객체는 다시 최상위 tree 객체에 의해 참조된다.

tree 객체를 사용한 버전 컨트롤

tree 객체는 버전 컨트롤의 대상이 되는 프로젝트의 서로 다른 스냅샷을 표현할 수 있다.

git 시스템은 스테이징 영역에 있는 상태로부터 tree객체를 생성하던가, 인덱스를 가져다가 tree 객체를 생성할 수 있다.

  • git update-index 명령어를 사용하면 처음 추가된 text.txt 파일을 새로이 스테이징 영역으로 올릴 수 있다.
    • 해당 파일이 아직 스테이징 영역에 존재하지 않으므로, 새 스테이징 영역에 추가하기 위해 --add 옵션을 더한다.
    • 해당 파일이 현재 디렉토리에는 존재하지 않지만 git object 저장소에는 저장하므로 --cacheinfo 옵션을 추가한다.
  • 최종 명령어는 아래와 같은 형태가 될 것이다.
git update-index --add --cacheinfo 100644 <추가하는 object hash key> <파일 이름>

git blob 타입은 아래 3개의 모드를 가진다.

위와 같이 update-index 명령어를 실행한 이후에는 git write-tree 명령어를 실행해서 스테이징 영역을 tree 객체로 만들 수 있다.

Commit Objects

위에서 알아봤던 것처럼, tree 객체를 사용함으로써 서로 다른 버전의 파일들과 복잡한 파일 구조를 가지는 프로젝트들을 하나의 tree 객체로 만들 수 있음을 알아보았다.

하지만 위 방식은 아래와 같은 단점이 존재한다.

  • 스냅샷을 꺼내기 위해서 복잡한 (보다 기계 친화적인) SHA-1 해시 값을 기억해야 한다.
  • 누가 스냅샷을 저장했는지 알 수 없다.
  • 왜 스냅샷을 저장했는지 알 수 없다.

commit 객체는 이렇게 tree 객체만으로는 알 수 없는 사실들을 저장하기 위해서 생겨났다.

commit 객체를 임의로 생성하기 위해 git commit-tree 명령어를 사용할 수 있다.

echo 'First commit' | git commit-tree d8329f
fdf4fc3344e67ab068f836878b6c4951e3b15f3d

commit 객체에 저장되는 포맷은 간단하다.

  • 커밋 시점의 프로젝트의 최상위 tree 객체 정보
  • 커밋 작성자의 정보
    • git configuration에 명시된 user.nameuser.email 정보
  • 한 줄 띄우고 (명시된 줄바꿈)
  • 커밋 메시지

이렇게 생성된 commit 객체는 우리가 많이 쓰는 git log 명령어를 통해서 조회할 수도 있다.

이렇게 git add 명령어와 git commit 명령어를 전혀 쓰지 않고도 파일을 커밋할 수 있음을 알아보았다!

  • 변경된 파일에 대한 blob 객체를 저장하고 (git hash-object)
  • 해당 blob 객체에 대한 인덱스를 업데이트 한 이후 (git update-index)
  • tree를 작성하고 (git write-tree)
  • 최상위 트리와 바로 직전 커밋을 참조하는 commit 객체를 생성했다. (git commit-tree)

이렇게 저장된 객체들은 전부 ./git/objects 디렉터리 하위에 저장된다.

아마 커밋을 생성한 시점 이후의 객체 그래프를 그려보면 아래와 같이 보일 것이다.

Object Storage

앞서 살펴봤던 git이 객체를 저장할 때 일종의 헤더를 사용한다고 했었다. blob 객체를 저장할 때 git 시스템은 아래 방법을 사용해서 객체를 저장한다.

(본문에는 좀 더 상세하게 언급되었지만 본 글에서는 간단하게만 다룬다)

  1. 객체 타입을 명세한다. (이 경우에는 blob)
  2. header에 컨텐츠의 byte 크기를 명세한다.
  3. 컨텐츠를 명세한다.
  4. 1+2+3 을 이어 붙인 데이터의 SHA-1 값을 계산한다. (이렇게 생성된 SHA-1 해시 값을 key로써 사용한다)
  5. 1+2+3 을 이어 붙인 데이터를 zlib을 사용하여 압축한다.
  6. 압축한 데이터를 디스크에 저장한다.
    1. 이 때 디렉터리 경로는 SHA-1 해시 값의 첫 두 문자가 된다.
    2. 40자 중 2자를 뺀, 나머지 38자를 파일 이름으로써 사용한다.
    3. zlib을 사용하여 압축한 데이터를 해당 파일에 저장한다.

잡담

git과 github? 다른 건가요?

git은 철저히 프로젝트의 버전 컨트롤을 위한 시스템이다.
github는 그렇게 버전 관리되는 프로젝트를 웹 상으로 공유하여 발전시키기 위한 웹 서비스이다.

git과 GitHub의 차이는 Java와 Javascript 만큼의 차이라고 할 수 있다.

git 상에서 버전 컨트롤되는 파일들은 삭제 또한 변경으로 본다.

  • blob 객체를 만들고, commit 객체로 tree 객체를 참조하게 되는 git 시스템의 특성
  • 때문에 중요한 secret 파일들은 버전 컨트롤되지 않도록 세심한 주의가 필요하다.

커밋되지 않고 로컬에 추가된 blob 객체를 버전 컨트롤하기

https://stackoverflow.com/questions/65632477/how-can-i-save-my-files-without-committing-with-git

https://stackoverflow.com/questions/54805875/git-push-all-objects-to-remote-repository-not-just-refs-but-absolutely-all-bl

위에서 살펴보았던 것처럼 commit 객체를 추가하지 않아도 로컬 git 시스템 상에 파일을 논리적으로 등록하고 저장할 수는 있다. 그렇다면 이렇게 저장된 파일을 다른 사람이 pull 해서 볼 수 있을까?

결론은 그렇지 않다. 커밋되지 않은 파일은 git gc의 대상이 된다.

  • git push 시점에 로컬 git 시스템은 저장된 다양한 객체들을 remote git 시스템에 업로드한다.
  • git 시스템 상 등록된 객체들을 보내는 과정 속에서, 이전의 해시 키 별 객체를 가지고 있는지, 필요한지 물어보는 과정이 수반된다.
  • 요청에 대한 티키타카를 하다가, 수신자가 해당 객체를 이미 가지고 있다면 해당 객체 수신을 거부하게 된다.
    • 이 경우 업로드 요청된 객체는 참조되지 않은 객체가 된다.
  • 수신하는 git 시스템은 요청 허용 / 거절을 반복하며 객체 데이터들을 전부 받는다.
    • 송신한 객체가 loose한 (참조되지 않은 객체)라면 이런 객체들은 기본적으로 14일 동안 유지 된다.
    • 하지만 송신자는 git 시스템 상 객체들을 묶어서 보내기 때문에, 수신자가 수신한 파일들의 묶음을 유지하기 위해서 다시 묶는 과정을 거친다면 참조되지 않은 객체들은 전부 삭제된다.
      • 이 때 참조되지 않는 객체들을 삭제하기 위해서 수신자는 git gc를 수행한다.

즉, 커밋되지 않고 로컬에 추가된 git object들은 remote 저장소에 영구적으로 저장될 수 없다.

github issue에 파일이 attach 되는 방식

github 이슈 또는 커멘트에 파일을 copy and paste 또는 첨부하여 편리하게 본문에 파일을 모두가 볼 수 있게 하는 기능은 anonymized url 방식을 사용한다.

  • 해당 방식은 오픈소스 url 프록시인 camo를 사용한다.
  • 즉 파일 첨부 시점에 파일은 이미 github 서버 상으로 업로드된다.
  • camo를 사용하여 업로드한 파일에 접근할 수 있는 url 프록시를 생성되며, 이 url 값은 업로드 api의 응답값으로 리턴된다.
  • 해당 업로드 api의 응답값으로 받게된 url은 '마치 파일이 url로 첨부된 것처럼' 본문에 링크된다.
  • 추측건데 github 본문 상에서 해당 방식으로 업로드된 그림 파일의 경우 바로 미리보기를 지원하는 fe 로직이 있지 않을까

관련하여 확인한 be 로직은 아래와 같았다. (github enterprise 기준, 상세 동작은 상이할 수 있습니다)
1. 이슈 또는 커멘트 본문에 파일 첨부시 POST /upload/policies/asset 를 한번 호출하여 해당 유저가 업로드할 권한이 있는지 체크하고
2. POST /user/{userNo}/files 을 호출하여 해당 유저의 private한 namespace에 업로드한 파일에 접근할 수 있는 url을 응답 값으로 주었다.
3. 해당 응답 값으로 오게 된 url은 바로 preview 될 수 있는 형태인 <img> 태그 안에 래핑되어 paste 된다.

github에 복사 + 붙여넣기되는 첨부 파일들은 첨부와 동시에 github 로그인된 계정의 리소스로 업로드 된다.

이후 업로드된 파일에 접근할 수 있는 github url을 자동으로 삽입함으로써 우리는 편리하게 이슈 및 커멘트에 첨부파일을 추가할 수 있었다.

(방금 이 글을 쓰면서 확인한건데, velog에서 첨부파일을 글 본문에 추가할 때에도 어느정도 유사한 방식을 사용한다 😉)

맺음말

  1. git은 버전 관리 시스템으로써, 여러 종류의 객체 데이터를 로컬 시스템(.git/objects) 에 저장함으로써 프로젝트의 버전을 관리할 수 있다.
  2. git addgit commit은 복잡한 객체 데이터 생성을 편리하게 해주는 일종의 명령어 집합이다.
  3. git과 github (또는 gitlab.. 또는 bitbucket..)을 같이 사용하여 보다 다채로운 협업이 가능하다. 다만 두 시스템은 목적이 다른 완전히 다른 시스템이다.
profile
make maketh install

1개의 댓글

comment-user-thumbnail
2024년 3월 3일

제목 센스에 무릎을 탁 치고 갑니다

답글 달기

관련 채용 정보