본문 바로가기

Git

Git의 내부 - 파일 상태와 Git 객체 알아보기

이번 포스팅에서는 Git의 내부에 대해서 살펴보겠습니다. 본 포스팅에 서술된 내용은 아래 내용을 참고하여 정리한 내용입니다.

https://git-scm.com/book/ko/v2/Git%EC%9D%98-%EB%82%B4%EB%B6%80-Git-%EA%B0%9C%EC%B2%B4

 

Git - Git 개체

여러분이 사용하는 쉘이 어떤 것인가에 따라 master^{tree} 표현식이 오류를 일으킬 수도 있다. Windows 에서 CMD는 ^ 문자는 이스케이프 기호로 사용한다. ^ 문자를 제대로 사용하려면 git cat-file -p master

git-scm.com

 

Git에서는 변경된 파일의 상태를 식별하고, 변경된 내용을 스냅샷의 형태인 커밋에 담아서 기록합니다. 파일에서 어떤 부분을 수정했는지 컴퓨터가 어떻게 식별해서 어떤 방식으로 내용을 저장한다는 걸까요? 이를 이해하기 위해선 Git의 내부를 이해할 필요가 있습니다. 크게 아래 두 가지 주제, Git의 파일 상태와 Git의 객체를 살펴보면 전체적인 흐름을 이해할 수 있습니다. 

Git의 파일 상태


Git의 파일 상태는 크게 Untracked, Modified, Staged, Unmodified 4가지로 나뉩니다.

Git Status 명령어를 통해서 파일의 상태를 확인할 수 있습니다. 아래 Git status 명령어의 결과를 통해서 어떻게 파일 상태를 나타내는지 살펴보겠습니다.

Git Status는 Working Directory와 Staging Area의 파일 상태를 보여주며, 이를 통해 Git이 어떤 파일들을 추적하고 있는지, 어떤 파일들이 변경되었는지, 어떤 파일들이 Staging Area에 추가되었는지 등을 확인할 수 있습니다. 명령어를 실행하면, 

"Changes not staged for commit"라는 메시지와 함께 "modified" 상태의 파일이 나열됩니다. 이어서 "Changes to be committed"라는 메시지와 함께 "staged" 상태의 파일도 나열됩니다. 마지막으로 추적하지 않는 파일인 Untracked 파일도 확인할 수 있습니다.

 

1. untracked

  • 변경사항을 추적하지 않는 파일을 의미
  • git add로 staging area에 추가되지 않은 파일은 추적되지 않습니다.
  • 버전 관리 대상에서 제외하기 위해 .gitignore에서 이름, 확장자 명시가 가능합니다.

2. unmodified

  • 변경사항이 없는 파일을 의미, 이전 버전과 비교해서 변경된 사항이 없으면 Git에서 변경 사항 추적을 하지 않습니다.

3. modified

  • 파일이 수정되어 이전 버전과는 다른 상태이지만, 아직 staging area에 추가되지 않아 커밋할 준비가 되지 않은 상태입니다.

4. staged

  • 파일이 수정되었으며, Staging Area에 추가된 상태를 의미합니다.
  • 이전 버전과 비교하여 변경된 사항이 있으며, Git이 해당 파일의 변경사항 추적을 하고 있으며, Staging Area에 추가되어 커밋할 준비가 된 상태입니다.

이러한 파일 상태의 흐름을 위의 그림처럼 나타낼 수 있습니다. 

 

Git의 객체들 - Blob, Tree, Commit


Git 저장소는 객체(Object)의 집합체입니다. 객체란 파일 내용이나 디렉토리 구조, 파일 메타데이터 등을 포함하는 Git에서 사용하는 데이터 유형입니다. Git 저장소는 각 객체를 고유한 해시 값(Hash value)으로 식별합니다.

 

Git의 객체에는 blob, commit, tree 등이 있습니다. 하나씩 살펴보며 Git의 내부가 어떻게 구성되어 버전 관리가 가능해지는지 알아봅시다.

 

Blob(Binary Large Object)은 Git에서 파일을 나타내는 최소 단위입니다. Blob은 파일의 크기와 내용을 고유한 해시 값으로 변환하여 저장합니다. 따라서 동일한 파일 내용을 가진 blob 객체는 동일한 해시 값을 가지게 됩니다.

예를 들어, "hello.txt"라는 이름의 파일이 있다고 가정해봅시다. 이 파일에는 "Hello, World!"라는 문자열이 포함되어 있습니다. 이 파일을 Git 저장소에 추가하면, Git은 이 파일을 blob 객체로 변환하여 저장합니다. 이 blob 객체는 "Hello, World!"라는 문자열과 이 문자열의 크기를 해시 함수로 변환한 고유한 해시 값으로 구성됩니다.

이제 같은 내용을 가진 다른 파일 "greetings.txt"을 추가해보겠습니다. 이 파일에도 "Hello, World!"라는 문자열이 포함되어 있습니다. 하지만 이전에 추가한 "hello.txt" 파일과 파일 이름만 다르고 내용은 동일합니다. Git은 이 파일을 blob 객체로 변환할 때, 이미 "Hello, World!"라는 문자열에 대한 해시 값을 가지고 있기 때문에 이 값을 그대로 사용하여 blob 객체를 생성합니다. 따라서 "hello.txt" 파일과 "greetings.txt" 파일은 동일한 blob 객체를 공유하게 됩니다.

이와 같이 Git에서는 파일의 내용이 변경되지 않으면 동일한 내용을 가진 blob 객체를 공유하여 저장하므로, 파일의 내용이 동일한 경우 중복 저장을 피할 수 있습니다. 이를 통해 Git 저장소의 용량을 절약할 수 있습니다. 또한, Git은 blob 객체의 해시 값을 사용하여 파일의 내용이 변경되었는지 여부를 검사하므로, 변경된 내용만 새로운 blob 객체로 저장하여 저장소 용량을 효율적으로 관리할 수 있습니다.

 

Tree 객체는 Git에서 디렉토리를 나타내는 객체입니다. Tree 객체는 blob 객체와 다른 tree 객체의 참조를 포함하여, 파일 시스템의 디렉토리 구조와 유사한 구조로 이루어져 있습니다. 따라서, tree 객체는 디렉토리 구조를 저장하는 중요한 객체입니다. 아래의 디렉토리 구조를 예를 들어 설명해보겠습니다.

src/
├── main/
│   ├── java/
│   │   ├── com/
│   │   │   └── example/
│   │   │       └── MyJavaFile.java
│   │   └── resources/
│   │       └── my-config.yaml
│   └── resources/
│       └── log4j.xml
└── test/
    └── java/
        └── com/
            └── example/
                └── MyJavaFileTest.java

위 구조를 Git으로 관리하기 위해서는, 각 파일과 디렉토리를 Blob 객체와 Tree 객체로 변환하여 저장해야 합니다. 예를 들어, "README.md" 파일은 Blob 객체로 변환되어 저장되고, "src" 디렉토리는 Tree 객체로 변환되어 저장됩니다.

 

이때, "src" 디렉토리의 Tree 객체는 "main" 디렉토리와 "test" 디렉토리를 가리키는 Tree 객체를 각각 가지고 있습니다. "main" 디렉토리의 Tree 객체는 "java" 디렉토리와 "resources" 디렉토리를 가리키는 Tree 객체를 각각 가지고 있습니다. "java" 디렉토리의 Tree 객체는 "com" 디렉토리를 가리키는 Tree 객체를 가지고 있습니다.

이러한 방식으로, Git에서는 파일 및 디렉토리의 구조를 Tree 객체의 계층 구조로 저장함으로써, 파일 및 디렉토리 간의 관계와 구조를 유지하면서 Git 저장소에서 파일을 관리할 수 있습니다.

 

Commit 객체는 Git 저장소의 히스토리를 관리하기 위한 핵심 객체 중 하나로, 파일 변경 사항을 저장하고 해당 변경 사항이 언제, 누가, 왜 수행되었는지에 대한 정보를 저장합니다. 

Commit 명령어를 실행하면, Git은 다음과 같은 작업을 수행합니다.

  1. 변경된 파일에 대한 스냅샷을 생성합니다.
  2. 커밋 메시지 등 Commit 정보를 포함하는 head와 함께, 스냅샷을 가리키는 Tree 객체를 참조하는 Commit 객체를 생성합니다.
  3. 새로 생성된 Commit 객체가 이전 커밋을 가리키도록 설정합니다.

정리하면, Commit 객체는 다음과 같은 정보를 가지고 있습니다.

  • Tree 객체: 변경된 파일에 대한 스냅샷을 가리키는 Tree 객체
  • Parent Commit 객체: 이전 커밋을 가리키는 Commit 객체
  • Author 정보: 커밋을 수행한 사람의 이름과 이메일 주소, 커밋을 수행한 시간
  • Committer 정보: 커밋을 적용한 사람의 이름과 이메일 주소, 커밋을 적용한 시간
  • 커밋 메시지: 변경 사항에 대한 설명을 담은 메시지

Commit 객체는 Git 저장소에서 변경 이력을 추적하는 데 중요한 역할을 합니다. Git은 Commit 객체의 계층 구조를 통해, 저장소의 히스토리를 관리합니다.

 

Branch

Branch는 Git 저장소에서 Commit 객체를 가리키는 포인터입니다. Branch는 Commit 객체의 참조를 저장하고, 작업이 완료된 후에는 새로운 Commit 객체를 생성하여 해당 Branch를 업데이트합니다. 즉, Branch는 Commit을 참조하고, Commit은 다른 Commit을 참조하는 방식으로 버전의 히스토리를 기록할 수 있습니다.

Git의 작동 방식


위의 내용들을 요약해서 Git의 working flow를 간단하게만 살펴보겠습니다.

 

1. 프로젝트에서 변경된 사항들이 발생합니다.

2. Git은 현재 Working directory와 이전에 Stage 되었던 객체의 비교를 통해서 수정된 사항을 파악합니다.

3. 사용자는 Snapshot으로 기록(Commit)하고자 하는 변경된 파일들을 스테이지에 올려 커밋할 준비를 합니다.

  • 프로젝트에서 각 파일들은 Blob 객체로 저장됩니다. Blob은 파일의 내용을 해시로 mapping하고 압축한 데이터입니다.
  • 프로젝트의 디렉토리 구조를 반영하여 Tree 객체를 생성합니다. Tree 객체는 blob을 참조한 트리 구조를 재귀적으로 포함하여 전체 프로젝트의 디렉토리 구조를 나타낼 수 있습니다.
  • Commit 객체는 Tree 객체와 Commit 정보, 메시지 지, 부모 커밋 객체를 포함하여 저장소의 스냅샷을 나타낼 수 있는 객체입니다. 최종적으로 커밋 객체 간의 참조 구조를 통해서 마치 연결 리스트가 작동하는 것처럼 버전을 추적하고 복원할 수 있는 것입니다.
  • Branch는 여기서 특정 커밋을 가리키는 포인터입니다. 새로운 Commit 객체가 생성되었으니 작업 중인 브랜치의 참조 위치를 업데이트합니다.

4. 스테이지에 올린 파일들은 커밋 객체에 저장되고, Stage는 초기화됩니다.

 

위의 Cycle로 Git의 버전 관리가 일어나며, 문제가 생기면 언제든지 커밋을 추적하여 원하는 커밋으로 데이터를 복원할 수 있습니다.