Featured image of post Git 객체로 살펴보는 add와 commit

Git 객체로 살펴보는 add와 commit

.git을 파헤쳐보자

Git은 분산형 버전 관리 시스템(Distributed Version Control System, DVCS)으로, 소스 코드와 파일의 변경 이력을 관리하는 도구입니다.

Git을 활용하려면 원하는 디렉토리 경로에 git init 명령을 실행하게되는데, 이때 .git 디렉토리가 만들어집니다.

.git 내부 구조

이 때 만들어지는 .git 디렉토리는 저장소(repository)의 메타데이터Git 객체들을 저장하는 장소이며, 그 중 Git 객체는 저장소의 데이터를 관리하고 추적하는데 핵심입니다.

GitGit 객체와 워킹 디렉토리의 파일들을 기준으로 변경을 감지하고, Git 객체의 정보를 통해 코드의 버전들을 관리하고 추적하게됩니다.

이를 조금 더 자세히 살펴보겠습니다.

Git 객체

.git 디렉토리 아래에 있는 /objects 디렉토리에 파일로 저장되는 Git 객체는 Git의 핵심으로, 모든 데이터의 내용과 실제 저장 위치를 의미하게 됩니다.

실제 /objects의 경로를 확인하면 아래와 같은 구조를 확인할 수 있습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/objects
├── 0d
│   └── df0ae059b771e502af67931f9f8a4a17632661
├── 24
│   └── 643cc37449b4bde54411a80b8ed61258225e34
├── 35
│   └── eb1ddfbbc029bcab630581847471d7f238ec53
├── 6c
│   └── 435e543633035641c810e97681b078d627a946
├── a6
│   └── d422786b4daf0d8cbd7466a02e4a4fa9fd8570
├── b1
│   └── 7d46df496e1a6aef89fc6d67937a2ba47b5d33
├── info
└── pack
    ├── pack-15a971ce7fbb8c9997e7e167347a23bbfe438d71.idx
    └── pack-15a971ce7fbb8c9997e7e167347a23bbfe438d71.pack

파일의 경로가 2글자로 시작하는 디렉토리 아래에 불특정한 38자 이름을 가진 파일이 저장되어있는데, 이 저장된 파일이 Git 객체입니다.

Git 객체의 이름 상태가…?

저장되는 파일 이름을 보시면 어느정도 눈치 채셨을 것 같은데요.

Git 객체가 위치와 이름은 총 40자, 16진수 문자열로 파일의 전체 내용을 SHA-1 함수로 해싱한 결과입니다.

이로인해 파일 내용의 무결성을 보장함은 물론 파일을 거의 유일하게 식별할 수 있습니다.

해시값을 사용하는 이유는 파일의 내용이 변경되면 아예 다른 결과가 반환되는 해시 함수의 특징을 활용해 파일의 변경을 간편하게 확인하기 위함입니다.

파일 내용이 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 해시 값을 가진다면, .git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391에 저장됩니다.

앞에 2자를 분리해 굳이 디렉토리로 만든 이유는 2가지 정도로 확인됩니다.

  • 성능 최적화
    • 한 디렉토리 내에 너무 많은 파일을 저장하면 성능이 저하됨
    • 수천 개 또는 수십만 개 파일을 하나의 디렉토리에 저장하면 검색, 파일 생성 및 접근 속도가 느려질 수 있음
    • 객체들을 고르게 분산하여 저장할 수 있어 성능 저하를 방지할 수 있음
  • 관리 용이
    • 디렉토리 구조로 나눔으로써 객체 파일을 관리하고 탐색하는 것이 더 쉬워짐(2자로 디렉터리를 바로 찾을 수 있음)

내용물은?

문서 혹은 파일의 내용으로 만들어지는 해시값을 경로로 저장되는 Git 객체 파일에는 해시값을 만드는데 사용하기도 했던 파일의 전체 내용을 압축하여 저장합니다.

Git 객체의 종류

Git 객체는 3가지 유형이 존재합니다.

  • 파일의 저장 경로와 파일의 내용은 모두 위와 동일하게 만들어집니다.

블롭(Blob)

  • 파일의 내용 자체를 저장
  • 파일의 이름이나 메타데이터는 저장하지 않고 파일의 원본 내용만 저장함

블롭 유형의 파일은 데이터 원본의 내용만을 압축하여 저장합니다.

압축하여 저장된 원본 데이터는 복구 등의 처리에서 활용됩니다.


트리(Tree)

  • 디렉터리의 구조를 저장
  • 디렉터리의 파일 및 하위 디렉터리와 그에 대한 블롭 객체 객체의 참조를 포함(트리는 블롭과 또 다른 트리로 구성됨)

트리 객체를 통해 디렉토리 구조를 관리할 수 있게 됩니다.

트리 객체가 의미하는 디렉토리의 존재하는 모든 파일, 디렉토리들의 경로(파일 주소)를 Git 객체의 값으로 압축하여 저장하게 됩니다.


커밋(Commit)

  • 커밋 정보를 저장
  • 커밋 메시지, 작성자, 날짜 및 커밋 시점의 트리 객체를 포함

직전 루트 트리 객체의 주소(파일 경로), 현재 루트 트리 객체의 주소(파일 경로)와 커밋 메시지 등 메타데이터를이 압축되어 저장됩니다.

git add

git add 명령어는 작업 디렉토리에서 변경된 파일들을 스테이징 영역에 추가하여 다음 커밋에 포함될 파일들을 준비하게됩니다.

  • 작업 디렉토리

    • 사용자가 작업 중인 실제 파일들이 존재하는 디렉토리
  • 인덱스(스테이징 영역)

    • 커밋을 준비하기 위해 변경된 파일들이 추가되는 영역
    • 인덱스는 .git/index 파일에 저장됨

먼저 변경된 파일들을 어떻게 관리하는지 확인해보겠습니다.

파일의 추적 상태

파일 상태 변화

Git은 작업 디렉토리의 파일을 4가지 상태로 추적, 관리합니다.

  • UnTracked: 추적되지 않는 파일
    • Git에 등록되지않아 추적되지 않는 상태
    • Git이 버전 관리하고 있지 않은 파일
    • 새로 생성된 파일이거나, 추적되지 않도록 명시적으로 설정된 파일(.gitignore)
  • Unmodified: 수정되지 않은 파일
    • Git이 추적하고 있는 파일이지만, 마지막 커밋 이후 변경되지 않은 상태
  • Modified: 수정된 파일
    • Git이 추적하고 있는 파일이지만, 마지막 커밋 이후로 파일의 내용이 변경된 상태
  • Staged: 스테이징된 파일
    • git add 명령을 통해 스테이징 영역에 추가된 상태
    • 다음 커밋에 포함될 준비가 되었다는 의미

Untracked -> Staged

Git으로 관리되고 있는 프로젝트에 처음 생성된 파일은 UnTracked 상태입니다. 즉 Git이 추적하고 있지 않은 상태임을 의미합니다.

git add 명령을 수행하면 해당 파일이 스태이징 영역에 추가되며 Staged 상태로 변경됩니다.


Unmodified -> Modified

Git을 통해 추적하고 있지만, 마지막 변경 이후 수정되지 않았다면 Unmodifed 상태입니다.

Unmodifed 상태의 파일에 변경이 발생하면(이전과 다른 부분이 발생하면) Modifed 상태가 됩니다.


Modified -> Staged

Unmodified 상태인 파일에 변경이 발생하여 Modifed 상태가 되었을 때 git add 명령로 스테이징 영역에 추가할 수 있습니다.

.git에서 일어나는 일

이제는 .git 내부에 어떤 일들이 발생하는지 살펴보겠습니다.

  1. 변경된 파일의 해시 계산
  • git add 명령어가 실행되면, Git은 현재 인덱스 파일(.git/index)의 기록된 정보를 활용하여 작업 디렉토리에서 변경된 파일의 내용을 찾아 읽습니다.
    • Untracked, Modified
  • 변경된 파일의 내용을 기반으로 SHA-1 해시를 계산하여 고유한 해시 값을 생성합니다.(Git 객체의 이름, 저장될 경로)
  1. 객체 생성 및 저장
  • 생성된 해시값을 이용하여 파일의 내용을 Git 객체로 저장합니다.(Blob)
    • Blob 객체는 파일의 바이너리 데이터를 담고 있으며, .git/obects 디렉토리에 저장
    • 저장 경로는 해시값의 처음 두자리를 디렉토리 이름으로 사용하고, 나머지 38자리를 파일 이름으로 사용하여 저장
  1. 인덱스 업데이트
    • Blob 객체가 생성되고 저장된 후, Git은 인덱스 파일(.git/index)을 업데이트합니다.
      • 파일의 모드, 해시 값, 파일 이름 등을 기록합니다.

index

index 파일은 stage 영역에 있는 파일들 즉 작업 디렉토리 내 추적중인 파일을 의미하며, 다음 commit에 포함될 파일의 정보들을 의미하게됩니다.

만약 git init 명령을 수행하고 README.mdsrc/index.js를 추가한 후 git add 명령을 수행했다면 .git/index 파일의 내용은 아래와 같이 수정됩니다.(해시 값은 임의로 설정하였습니다.)

1
2
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 README.md
100644 24643cc37449b4bde54411a80b8ed61258225e34 0 src/index.js

띄어쓰기로 구분되는 각 요소는 파일 모드, 해시값, 스테이지 번호, 파일 경로를 의미합니다.

여기서 주의해야할 점은 .git/index에는 Blob 객체만 저장된다는 점 입니다.


파일 모드

맨 앞의 100644는 파일 식별자로 흔히 알려진 파일의 권한을 포함하게됩니다.

그 역할은 아래와 같습니다.

  • 읽기, 쓰기 파일(블롭): 100644 (chmod 644)
  • 실행 파일(블롭): 100755 (chmod 755)
  • 디렉터리(트리): 040000

.git/indexBlob 객체 정보만 관리하므로 읽기 파일, 실행 파일만 저장됩니다.

Tree 객체를 를 의미하는 040000는 이후 커밋에 활용됩니다.


스테이지 번호

스테이지 번호는 병합 충돌 시 사용되는 필드입니다. 파일이 정상적으로 추가되었을 때는 0 입니다.

  • 0: 기본 스테이지. 충돌이 없는 파일이 여기에 위치합니다.
  • 1: 공통 조상 (ancestor) 버전.
  • 2: 현재 브랜치 (our) 버전.
  • 3: 병합하려는 브랜치 (their) 버전.

.git/index의 역할

인덱스 파일의 역할을 정리해보면 아래와 같습니다.

  • 인덱스 파일은 다음 커밋에 포함될 파일들을 추적합니다.
  • 인덱스에 추가된 파일들은 다음 git commit 명령어가 실행될 때 커밋 히스토리에 포함됩니다.

git commit

git commit 명령어가 실행되었을 때 어떤 흐름이 발생하는지 확인해보겠습니다.

  1. Tree 객체 생성
    • 인덱스 파일에 기록된 내용을 바탕으로 트리 객체가 생성됩니다.
    • 트리 객체는 디렉토리 구조와 파일 정보를 담고 있으며, 각 파일은 Blob 객체와 Tree 객체를 가리킵니다.
  2. Commit 객체 생성
    • 트리 객체가 생성된 후, 커밋 객체가 생성됩니다.
      • 트리 객체의 해시 값
      • 부모 커밋(이전 커밋)의 해시 값(최초 커밋인 경우 제외)
      • 커밋 메시지
      • 작성자와 커미터 정보(이름, 이메일, 타임스탬프 등)
  3. 객체 저장 및 참조 업데이트
    • 객체 저장
      • 생성된 커밋 객체와 트리 객체는 .git/objects 디렉토리에 저장됩니다.
      • Blob 객체와 마찬가지로 각 객체는 해시값을 기반으로 저장됩니다.
    • 참조 업데이트
      • 새로운 커밋 객체가 생성되면, 현재 브랜치의 참조가 새로운 커밋을 가리키도록 업데이트 합니다.
        • 현재 브랜치의 참조는 .git/refs/heads/<브랜치 이름> 파일에 저장
        • main인 경우 .git/refs/heads/main 파일의 내용이 새로운 커밋의 해시값으로 업데이트

새 커밋이 생성되면서 Git은 버전 히스토리를 관리합니다.

각 커밋 객체는 이전 커밋 객체(부모 객체)를 가르키며, 이를 통해 커밋 히스토리가 마치 연결 리스트 형태로 구성됩니다.

Tree 객체의 구성

처음 git commit 명령이 수행되면 작업 디렉토리를 기반으로 트리 객체를 만들게됩니다.

트리 객체는 특정 디렉터리 내부에 포함되는 모든 파일(Blob 객체), 디렉토리(Tree 객체)의 해시값을 내용으로 가지며, 작업 디렉토리가 아래와 같은 구조를 가진다고 가정하고 내부 구조를 확인해보겠습니다.

1
2
3
4
project/
├── README.md
└── src/
    └── index.js

작업 디렉터리의 루트 Tree 객체는 아래와 같은 내용을 가지게됩니다.(해시값은 임의로 작성하였습니다.)

1
2
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 README.md
040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea0ea1e1 src

src 디렉토리를 의미하는 Tree 객체는 아래와 같은 내용을 가집니다.

1
100644 24643cc37449b4bde54411a80b8ed61258225e34 index.js

각 내용들이 디렉토리를 구성하는 Git 객체의 주소값을 가르키게됩니다.

Commit 객체의 구성

Tree 객체가 만들어지면 작업 디렉토리의 Tree 객체(루트 Tree 객체)의 주소를 이용하여 Commit 객체를 만들게됩니다.

Commit 객체의 내용에는 작업 디렉토리의 루트 Tree 객체의 해시값, 부모 커밋의 해시값, 커밋 메시지, 작성자와 커밋터의 정보가 담깁니다.

1
2
3
4
5
6
tree <작업 디렉토리의 Tree 객체의 해시 값>
parent <이전 커밋의 해시 값(첫 커밋이라면 생략)>
author John Doe <john.doe@example.com> 1609459200 +0000
committer John Doe <john.doe@example.com> 1609459200 +0000

<커밋 메시지>

요약

  • git commit 명령어는 스테이징 영역의 파일들을 기반으로 트리 객체를 생성하고, 이를 참조하는 커밋 객체를 생성합니다.
  • 생성된 커밋 객체는 .git/objects 디렉토리에 저장되며, 현재 브랜치의 참조가 새로운 커밋 객체를 가리키도록 업데이트됩니다.
  • 이 과정은 Git의 분산 버전 관리 시스템이 효율적으로 파일의 변경 이력을 관리하고 추적할 수 있게 합니다.

마무리

이전부터 .git의 존재는 알았지만, 왜 존재하는지, 어떻게 구성되어있는지, 역할이 뭔지 등에 대해서는 큰 관심이 없었습니다.

이번 기회를 통해 .git의 내부 파일들(Git 객체)의 상태 변화를 확인하면서 addcommit 명령이 어떻게 수행되는지 더 깊이 이해할 수 있어 좋은 경험이었네요 😁

읽어보시고 잘못된 정보나, 이해가 어려운 부분이 있다면 댓글 남겨주세요!

틀린 내용이 있다면 빠르게 반영하고, 이해가 어려운 부분은 같이 다시 고민해볼 기회가 될 수 있을것 같습니다🔥

끝까지 읽어주셔서 감사합니다.😊