Git은 리누스 토르발스가 리눅스 커널 개발을 위해 2005년에 만든 분산 버전 관리 시스템이다. 소프트웨어 개발 과정에서 발생하는 소스 코드의 변경 이력을 체계적으로 관리하고, 여러 개발자가 동시에 작업하는 협업을 효율적으로 지원하는 도구이다.
기존의 중앙집중식 버전 관리 시스템과 달리, Git은 각 개발자의 로컬 컴퓨터에 전체 프로젝트 히스토리와 메타데이터를 복제하는 완전한 분산 구조를 가진다. 이는 네트워크 연결 없이도 대부분의 작업을 수행할 수 있게 하며, 중앙 서버에 장애가 발생해도 로컬 저장소에서 작업을 계속할 수 있는 높은 가용성을 제공한다. Git의 핵심 설계 철학은 속도, 데이터 무결성, 분산된 비선형 워크플로우를 지원하는 데 있다.
Git은 파일의 변화를 델타 방식이 아닌, 프로젝트의 특정 시점 전체 상태를 가리키는 스냅샷의 연속으로 관리한다. 변경사항이 발생할 때마다 변경된 파일의 새 스냅샷을 생성하고, 이전 스냅샷에 대한 링크를 저장한다. 이 방식은 브랜치 생성과 병합이 매우 빠르고 가벼우며, 프로젝트 히스토리를 비순환 방향 그래프 형태로 유지하게 한다.
오늘날 Git은 개인 프로젝트부터 대규모 기업 프로젝트에 이르기까지 사실상의 표준 버전 관리 시스템으로 자리 잡았다. GitHub, GitLab, Bitbucket과 같은 호스팅 서비스와 결합되어, 현대적인 소프트웨어 개발 및 DevOps 생태계의 근간을 이루고 있다.
Git은 분산 버전 관리 시스템의 한 종류이다. 중앙 서버에 의존하는 기존 시스템과 달리, 각 개발자의 로컬 환경에 전체 프로젝트 히스토리와 메타데이터를 복제하여 독립적으로 작업할 수 있다. 이는 네트워크 연결이 없어도 대부분의 작업이 가능하며, 중앙 서버 장애 시에도 로컬 히스토리를 기반으로 작업을 계속할 수 있는 장점을 제공한다.
Git은 파일의 변화를 관리할 때 델타 방식 대신 스냅샷 방식을 사용한다. 각 커밋이 이루어질 때, 프로젝트 내 변경된 파일 전체의 스냅샷을 기록하고, 이전 커밋에 대한 링크를 저장한다. 변경되지 않은 파일은 이전 스냅샷에 대한 링크만 저장하여 공간을 효율적으로 사용한다. 이 방식은 브랜치 생성과 병합을 매우 빠르게 만든다.
Git의 작업 흐름은 크게 세 가지 영역으로 구분된다.
영역 | 설명 |
|---|---|
사용자가 실제로 파일을 편집하는 프로젝트의 로컬 디렉토리이다. | |
스테이징 영역 (인덱스) | 다음 커밋에 포함될 변경사항들이 임시로 저장되는 곳이다. |
저장소 (.git 디렉토리) | 스테이징된 내용을 영구적으로 저장하는 데이터베이스이다. |
이 세 단계를 거치는 작업 흐름은 변경사항을 세밀하게 제어하고, 논리적으로 관련된 변경들만 하나의 커밋으로 묶을 수 있게 해준다.
분산 버전 관리 시스템(DVCS)은 중앙 서버에 의존하지 않고, 각 개발자의 로컬 환경에 전체 프로젝트 히스토리와 메타데이터를 완벽하게 복제하는 방식을 말한다. 이는 Git의 가장 근본적인 설계 철학이자 핵심 특징이다. 중앙집중식 시스템(CVCS)이 단일 서버를 진리의 원천으로 삼는 것과 달리, 분산 시스템에서는 모든 개발자의 작업 복사본이 완전한 저장소가 된다.
이러한 구조는 여러 가지 중요한 장점을 제공한다. 첫째, 대부분의 작업(커밋, 브랜치 생성, 히스토리 조회 등)이 네트워크 연결 없이 로컬에서 빠르게 수행될 수 있다. 둘째, 중앙 서버에 장애가 발생하더라도, 어떤 개발자의 로컬 저장소라도 전체 프로젝트를 복원하는 데 사용될 수 있어 가용성이 매우 높다. 셋째, 작업 흐름이 유연해져서, 개발자는 로컬에서 자유롭게 실험하고 브랜치를 만들며, 작업이 완성된 후에만 다른 동료나 중앙 저장소와 변경 사항을 교환한다.
분산 모델은 협업 방식에도 변화를 가져왔다. 중앙 저장소는 여전히 공식적인 프로젝트의 '기준점' 역할을 할 수 있지만, 협업은 중앙-위계적 구조뿐만 아니라 다양한 형태로 이루어진다. 예를 들어, 개발자 A는 개발자 B의 저장소에서 직접 변경 사항을 가져올(pull) 수 있으며, 이는 오픈 소스 프로젝트에서 흔히 볼 수 있는 포크(fork)와 풀 리퀘스트(pull request) 모델의 기반이 된다.
대부분의 다른 버전 관리 시스템은 각 파일의 변경 이력을 델타 기반으로 저장합니다. 이 방식은 파일의 초기 버전을 저장한 후, 이후 각 버전마다 이전 버전과의 차이점만을 기록합니다. 반면, Git은 데이터를 스냅샷의 스트림으로 취급합니다.
Git이 커밋을 수행할 때, 프로젝트의 모든 파일 상태에 대한 스냅샷을 찍어 이를 데이터베이스에 저장합니다. 효율성을 위해, 파일 내용이 변경되지 않았다면 Git은 이전에 저장된 동일한 파일에 대한 링크만 저장합니다. 이 방식은 델타 기반 시스템과 근본적으로 다릅니다.
접근 방식 | 설명 | 주요 특징 |
|---|---|---|
델타 기반 | 각 버전을 이전 버전과의 차이(델타)로 저장합니다. | 특정 시점의 전체 상태를 보려면 초기 버전부터 모든 델타를 순차적으로 적용해야 합니다. |
스냅샷 기반 | 각 커밋 시점의 전체 파일 상태(스냅샷)을 저장합니다. | 각 커밋은 프로젝트 특정 시점의 완전한 기록을 독립적으로 나타냅니다. |
이 스냅샷 방식은 Git의 여러 동작 특성을 결정합니다. 예를 들어, 대부분의 작업이 로컬에서 매우 빠르게 이루어질 수 있는 이유는 히스토리를 조회할 때 델타를 계산할 필요 없이 직접적으로 스냅샷에 접근할 수 있기 때문입니다. 또한, 브랜치 생성과 병합이 가볍고 빠른 이유도 새로운 브랜치는 단순히 특정 스냅샷을 가리키는 포인터에 불과하기 때문입니다.
데이터 무결성 측면에서 Git은 스냅샷을 저장하기 전에 모든 데이터와 그에 대한 헤더를 SHA-1 해시를 사용하여 체크섬을 구한 후, 그 체크섬으로 데이터를 참조합니다. 이는 파일의 내용이나 디렉토리 구조가 조금이라도 변경되면 완전히 다른 체크섬이 생성됨을 의미합니다. 따라서 Git은 체크섬을 통해 모든 데이터를 식별하고 저장하여 데이터의 손상이나 변조를 방지합니다.
Git의 작업 흐름은 크게 세 가지 영역으로 구분된다. 바로 워킹 디렉토리(Working Directory), 스테이징 영역(Staging Area, 또는 Index), 그리고 저장소(Repository)이다. 이 세 영역은 파일의 상태를 관리하고 변경 이력을 기록하는 핵심적인 단계를 정의한다.
워킹 디렉토리는 사용자가 실제로 파일을 편집하고 작업하는 프로젝트 폴더를 가리킨다. 여기서 파일을 생성, 수정, 삭제하면 해당 파일은 '수정됨(Modified)' 상태가 된다. 이 상태의 파일은 아직 Git의 관리 대상으로 확정되지 않았다. 스테이징 영역은 다음 커밋(Commit)에 포함될 변경사항들을 임시로 모아두는 곳이다. git add 명령어를 사용하면 워킹 디렉토리의 변경사항이 스테이징 영역으로 이동하여 '스테이징됨(Staged)' 상태가 된다. 이 영역을 통해 사용자는 어떤 변경사항을 다음 기록에 남길지 세밀하게 선택하고 준비할 수 있다.
마지막으로 저장소는 스테이징 영역의 내용을 영구적인 스냅샷으로 기록하는 최종 단계이다. git commit 명령어를 실행하면 스테이징 영역에 모인 모든 변경사항이 하나의 커밋 객체로 변환되어 로컬 저장소(.git 디렉토리)에 안전하게 저장된다. 이때 파일은 '커밋됨(Committed)' 상태가 된다. 이 세 단계의 일반적인 흐름은 다음과 같이 요약할 수 있다.
작업 영역 | 명령어 | 파일 상태 | 설명 |
|---|---|---|---|
워킹 디렉토리 | (파일 편집) | Modified | 실제 파일이 수정된 상태 |
스테이징 영역 |
| Staged | 다음 커밋에 포함될 변경사항이 준비된 상태 |
저장소 |
| Committed | 변경사항이 영구적인 스냅샷으로 저장된 상태 |
이 구조는 Git이 변경사항을 관리하는 방식을 명확히 구분하여, 사용자가 자유롭게 작업한 후 원하는 시점에 정리된 기록을 남길 수 있게 해준다. 스테이징 영역의 존재 덕분에 한 번의 커밋에 서로 다른 파일의 변경 이력을 논리적으로 묶거나, 불필요한 파일을 제외하는 세밀한 제어가 가능해진다.
Git의 핵심 명령어는 크게 초기화, 변경사항 추적, 브랜치 관리, 원격 저장소 작업, 히스토리 조회 및 되돌리기로 분류할 수 있다. 이 명령어들은 워킹 디렉토리, 스테이징 영역, 저장소라는 세 가지 주요 영역 간의 데이터 흐름을 관리한다.
초기화 및 설정 명령어로는 git init과 git config가 있다. git init은 새로운 Git 저장소를 생성하며, 이 명령어를 실행하면 현재 디렉토리에 숨겨진 .git 디렉토리가 만들어진다. git config는 사용자 이름, 이메일, 에디터, 별칭 등 Git의 동작을 설정하는 데 사용된다. 변경사항 추적 및 커밋을 위한 기본 명령어는 다음과 같다.
명령어 | 주요 기능 |
|---|---|
| 워킹 디렉토리와 스테이징 영역의 상태를 확인한다. |
| 지정된 파일의 변경사항을 스테이징 영역에 추가한다. |
| 스테이징 영역의 내용을 저장소에 새로운 커밋으로 기록한다. |
| 커밋 메시지를 함께 작성하여 바로 커밋을 생성한다. |
| 워킹 디렉토리와 스테이징 영역 간의 차이를 보여준다. |
| 스테이징 영역과 최신 커밋 간의 차이를 보여준다. |
브랜치 및 병합 작업에는 git branch, git checkout, git switch, git merge가 핵심이다. git branch는 브랜치 목록을 보거나 새로운 브랜치를 생성한다. git checkout <브랜치명> 또는 git switch <브랜치명>은 지정된 브랜치로 워킹 디렉토리를 전환한다. git merge <브랜치명>은 현재 브랜치에 지정된 브랜치의 변경 이력을 통합한다. 원격 저장소와 협업하기 위해서는 git clone, git remote, git fetch, git pull, git push 명령어를 사용한다. git clone은 원격 저장소를 로컬에 복제한다. git remote -v는 연결된 원격 저장소 목록을 확인한다. git fetch는 원격 저장소의 변경사항을 로컬에 다운로드만 하고, git pull은 fetch 후 자동으로 병합을 수행한다. git push는 로컬의 커밋을 원격 저장소에 업로드한다.
히스토리를 조회하고 되돌리는 명령어도 다양하다. git log는 커밋 히스토리를 다양한 옵션으로 조회한다. git log --oneline --graph는 히스토리를 간략하고 시각적으로 보여준다. 변경사항을 취소할 때는 단계에 따라 명령어가 다르다. 워킹 디렉토리의 파일 변경을 최신 커밋 상태로 되돌리려면 git checkout -- <파일>을 사용한다. 스테이징 영역에서 파일을 제거하려면 git reset HEAD <파일>을 실행한다. 이미 생성된 커밋을 수정하거나 취소할 때는 git commit --amend로 최신 커밋을 재작성하거나, git reset을 사용하여 특정 커밋으로 상태를 되돌린다.
Git 저장소를 사용하기 위한 첫 단계는 저장소를 초기화하거나 기존 저장소를 복제하는 것이다. git init 명령어는 현재 디렉토리를 새로운 Git 저장소로 초기화한다. 이 명령어를 실행하면 .git이라는 하위 디렉토리가 생성되며, 여기에 모든 객체 모델과 참조(Refs) 데이터가 저장된다. 반면, git clone <저장소 URL> 명령어는 기존의 원격 저장소를 로컬 머신에 복사한다. 이 명령어는 원격 저장소의 전체 히스토리와 모든 브랜치를 가져와 로컬에 동일한 환경을 구성한다.
사용자 정보와 기본 동작을 설정하는 것은 협업과 기록 관리에 필수적이다. git config 명령어를 사용하여 전역 또는 로컬 저장소별 설정을 할 수 있다. 주요 설정 항목은 다음과 같다.
설정 범위 | 명령어 예시 | 설명 |
|---|---|---|
전역 설정 |
| 모든 저장소에 적용될 사용자 이름을 설정한다. |
전역 설정 |
| 모든 저장소에 적용될 사용자 이메일을 설정한다. |
로컬 설정 |
| 현재 저장소에만 적용될 사용자 이름을 설정한다. |
시스템 설정 |
| 시스템의 모든 사용자에게 적용될 기본 텍스트 에디터를 설정한다. |
또한, 기본 브랜치 이름을 main으로 변경하거나, 커밋 시 자동으로 줄 바꿈을 처리하는 방식(core.autocrlf), 병합 도구 등을 설정할 수 있다. 설정값은 ~/.gitconfig 파일(전역), .git/config 파일(로컬) 등에 저장된다.
워킹 디렉토리에서 파일을 수정하면, Git은 이를 '수정됨(modified)' 상태로 인식합니다. git status 명령을 실행하면 이러한 변경 사항을 확인할 수 있습니다. 추적되지 않는 새 파일은 '추적 안 됨(untracked)'으로 표시됩니다.
변경 사항을 저장소에 기록하려면 먼저 스테이징 영역(인덱스)에 추가해야 합니다. git add <파일명> 명령은 특정 파일의 현재 스냅샷을 스테이징 영역으로 올립니다. git add . 명령은 모든 변경 사항을 한꺼번에 추가할 때 사용합니다. 이 단계를 거친 파일은 '스테이징됨(staged)' 상태가 됩니다.
스테이징 영역에 모인 변경 사항은 git commit 명령으로 영구적인 스냅샷으로 저장됩니다. 이 명령을 실행하면 편집기가 열려 커밋 메시지를 작성하게 됩니다. -m 옵션을 사용하면 명령줄에서 바로 메시지를 첨부할 수 있습니다(예: git commit -m "기능 추가: 로그인 검증 로직 구현"). 각 커밋은 고유한 SHA-1 해시 값, 작성자 정보, 이전 커밋에 대한 링크, 그리고 스테이징 영역의 내용을 기반으로 생성된 트리 객체를 포함합니다.
주요 명령어의 흐름과 효과는 다음과 같이 정리할 수 있습니다.
명령어 | 주요 역할 | 영향 범위 |
|---|---|---|
| 워킹 디렉토리와 스테이징 영역의 상태를 확인 | 정보 제공 |
| 커밋되지 않은 변경 내용(워킹 디렉토리 vs 스테이징)을 비교 | 정보 제공 |
| 스테이징 영역의 내용과 최신 커밋을 비교 | 정보 제공 |
| 특정 파일의 변경 사항을 스테이징 영역에 추가 | 워킹 디렉토리 → 스테이징 영역 |
| 스테이징 영역의 내용을 저장소에 새로운 스냅샷으로 기록 | 스테이징 영역 → 저장소 |
| 파일을 스테이징 영역에서 제거(추적 중지)하지만 워킹 디렉토리는 유지 | 스테이징 영역에서 제거 |
git commit -a 옵션은 이미 추적 중인(tracked) 파일의 수정 사항을 자동으로 스테이징하고 커밋하는 단축 명령입니다. 그러나 새로 생성된 파일(untracked)은 이 옵션으로 추가되지 않습니다.
브랜치는 개발 흐름을 독립적으로 관리하기 위한 포인터이다. 기본적으로 main 또는 master라는 이름의 브랜치에서 시작하며, 새로운 기능 개발이나 버그 수정을 위해 별도의 브랜치를 생성하여 작업한다. 이는 안정적인 메인 라인을 유지하면서 병렬 개발을 가능하게 한다.
브랜치 관련 주요 명령어는 다음과 같다.
명령어 | 설명 |
|---|---|
| 로컬 브랜치 목록을 조회한다. |
| 새로운 브랜치를 생성한다. |
| 지정한 브랜치로 워킹 디렉토리를 전환한다. |
|
|
| 현재 브랜치에 지정한 브랜치의 변경 이력을 통합한다. |
병합은 일반적으로 두 가지 방식으로 수행된다. 첫째, Fast-forward 병합은 현재 브랜치가 병합 대상 브랜치의 직접적인 조상일 때, 단순히 브랜치 포인터를 앞으로 이동시킨다. 둘째, 3-way 병합은 두 브랜치의 공통 조상과 각 브랜치의 최신 커밋을 비교하여 새로운 병합 커밋을 생성한다. 병합 과정에서 같은 파일의 같은 부분을 수정한 경우 충돌이 발생하며, 사용자가 직접 수정한 후 커밋하여 해결해야 한다.
병합 외에 브랜치 통합 방법으로 리베이스가 있다. git rebase <branch-name> 명령어는 현재 브랜치의 커밋들을 대상 브랜치의 최신 커밋 뒤로 재배치한다. 이는 병합 커밋 없이 선형적인 히스토리를 만들지만, 이미 공유된 커밋의 히스토리를 재작성할 경우 문제를 일으킬 수 있다.
원격 저장소 작업은 분산 버전 관리 시스템인 Git의 핵심 기능으로, 로컬 저장소와 외부 서버에 있는 저장소 간의 동기화를 관리한다. 주로 origin이라는 별칭으로 설정된 중앙 서버와의 협업을 위해 사용된다.
주요 명령어는 다음과 같다. git remote add <이름> <URL> 명령어로 새로운 원격 저장소 연결을 추가한다. git push <원격저장소> <브랜치>는 로컬 브랜치의 커밋 히스토리를 원격 저장소로 업로드한다. 반대로 git fetch <원격저장소>는 원격 저장소의 최신 변경 내역을 로컬로 가져오지만, 워킹 디렉토리에 자동으로 병합하지는 않는다. git pull 명령어는 git fetch 후 git merge를 자동으로 수행하는 편의 명령어이다. git clone <URL>은 기존 원격 저장소의 전체 히스토리와 메타데이터를 복제하여 새로운 로컬 저장소를 생성하는 데 사용된다.
명령어 | 주요 기능 |
|---|---|
| 원격 저장소 연결 추가 |
| 로컬 커밋을 원격 저장소로 업로드 |
| 원격 변경사항을 로컬에 다운로드 (병합 없음) |
| 원격 변경사항을 가져와 로컬 브랜치에 병합 |
| 원격 저장소를 로컬에 복제 |
원격 브랜치는 origin/main과 같은 형식으로 참조된다. git push 시 충돌이 발생하면, 일반적으로 원격 브랜치의 최신 변경사항을 먼저 git pull로 병합한 후 다시 시도한다. git remote -v 명령으로 현재 설정된 모든 원격 저장소 목록과 URL을 확인할 수 있다.
Git에서 작업 내역을 검토하거나 이전 상태로 복원하는 명령어는 프로젝트 히스토리를 관리하는 데 필수적이다. 주요 조회 명령어로는 git log가 있다. 이 명령어는 커밋 해시, 작성자, 날짜, 메시지를 보여주며, --oneline, --graph, --stat과 같은 옵션으로 출력을 다양하게 포맷할 수 있다. 특정 파일의 변경 이력만 보려면 git log -- <파일경로>를 사용한다. git show <커밋> 명령은 특정 커밋의 상세 변경 내용을 diff 형식으로 보여준다.
되돌리기 작업은 크게 커밋을 수정하는 작업과 저장소 상태를 과거로 되돌리는 작업으로 나눌 수 있다. 가장 최근 커밋을 수정하려면 git commit --amend를 사용한다. 이 명령은 스테이징 영역의 변경사항을 최신 커밋에 추가하거나 커밋 메시지만 재작성할 때 사용한다. 단, 이미 원격 저장소에 푸시된 커밋을 수정하는 것은 협업에 문제를 일으킬 수 있다.
과거 상태로 복원하는 대표적인 명령어는 git reset, git revert, git checkout이다. 각 명령어는 동작 방식과 영향 범위가 다르다.
명령어 | 주요 용도 | 동작 대상 | 공개된 히스토리 변경 여부 |
|---|---|---|---|
| 현재 브랜치의 HEAD를 지정 커밋으로 이동. 이후 커밋을 삭제하거나 스테이징 해제. | 현재 브랜치 (HEAD) | 예 (주의 필요) |
| 지정 커밋의 변경사항을 반대하는 새로운 커밋을 생성. | 모든 브랜치 | 아니오 (안전함) |
| 워킹 디렉토리의 특정 파일을 최신 커밋 또는 스테이징 영역 상태로 복원. | 워킹 디렉토리 파일 | 아니오 |
git reset은 --soft, --mixed(기본값), --hard 옵션에 따라 다르게 동작한다. --soft는 HEAD만 이동시키고, --mixed는 HEAD 이동과 함께 스테이징 영역을 해제하며, --hard는 HEAD 이동과 함께 워킹 디렉토리와 스테이징 영역을 모두 해당 커밋 상태로 덮어쓴다. --hard 옵션은 추적되지 않은 파일을 제외한 모든 로컬 변경사항을 영구히 삭제하므로 각별한 주의가 필요하다. 반면, git revert는 기존 커밋을 없애지 않고, 그 변경을 취소하는 새로운 커밋을 생성하므로 공유된 히스토리를 안전하게 되돌릴 때 사용한다.
.git 디렉토리는 Git 저장소의 핵심이며, 모든 버전 관리 데이터와 메타데이터가 이 디렉터리 안에 저장된다. 주요 구성 요소는 다음과 같다.
디렉토리/파일 | 설명 |
|---|---|
| |
| 브랜치와 태그와 같은 참조(Refs)에 대한 포인터 파일이 저장된다. |
| 현재 작업 중인 브랜치 또는 특정 커밋을 가리키는 포인터 파일이다. 일반적으로 |
| 스테이징 영역의 내용을 저장하는 바이너리 파일이다. 다음 커밋에 포함될 파일들의 정보를 담고 있다. |
| 해당 저장소에 특화된 설정 정보를 저장하는 파일이다. |
| 특정 Git 이벤트 발생 시 자동으로 실행되는 스크립트([2])를 저장하는 디렉터리이다. |
Git의 객체 모델은 네 가지 기본 객체 타입으로 구성된다. Blob 객체는 파일의 내용 자체를 저장한다. 파일 이름이나 디렉터리 구조 정보는 포함하지 않으며, 순수한 데이터 덩어리이다. Tree 객체는 하나의 디렉터리 스냅샷을 나타낸다. 이 객체는 Blob이나 다른 Tree에 대한 참조와 해당 항목의 이름, 권한을 기록하여 디렉터리 구조를 정의한다.
Commit 객체는 프로젝트 히스토리의 한 지점을 기록한다. 이 객체는 특정 시점의 최상위 Tree 객체에 대한 참조, 부모 커밋에 대한 참조(초기 커밋 제외), 작성자 정보, 커밋 메시지 등을 포함한다. Tag 객체는 특정 커밋(주로 릴리스 버전)에 영구적인 이름을 붙이는 데 사용되며, 태그 생성자, 날짜, 메시지와 대상 객체를 참조한다. 모든 객체는 내용을 SHA-1 해시한 값을 고유 식별자로 사용한다.
참조 시스템은 이러한 객체들을 인간이 이해하기 쉬운 이름으로 접근할 수 있게 한다. 브랜치(refs/heads/ 아래)는 개발 라인의 최신 커밋을 가리키는 이동 가능한 포인터이다. 태그(refs/tags/ 아래)는 일반적으로 특정 커밋을 가리키는 불변의 포인터이다. HEAD는 현재 작업 공간이 어떤 브랜치나 커밋을 바라보고 있는지를 정의하는 특수한 참조이다. 이러한 참조들은 내부적으로 단순히 SHA-1 해시 값을 담은 텍스트 파일로 존재한다.
Git 저장소의 핵심은 .git 디렉토리이다. 이 숨겨진 디렉토리는 프로젝트의 모든 버전 관리 정보와 메타데이터를 담고 있다. git init 명령어를 실행하면 생성되며, 여기에는 객체 데이터베이스, 참조(Refs), 설정 파일, 임시 파일 등이 체계적으로 구성되어 있다.
.git 디렉토리의 주요 구성 요소는 다음과 같다.
디렉토리/파일 | 설명 |
|---|---|
| Blob, Tree, Commit, Tag 객체가 저장되는 객체 데이터베이스이다. 각 객체는 SHA-1 해시값의 처음 두 글자를 디렉토리명으로, 나머지를 파일명으로 하여 저장된다. |
| 참조(Refs)가 저장되는 디렉토리이다. |
| 현재 작업 중인 브랜치 또는 특정 커밋을 가리키는 참조 파일이다. 일반적으로 |
| 스테이징 영역의 내용을 담고 있는 바이너리 파일이다. 다음 커밋에 포함될 파일들의 상태(해시, 경로, 타임스탬프 등)를 기록한다. |
| 해당 저장소에 특화된 Git 설정 파일이다. 사용자 정보, 원격 저장소 주소, 에일리어스 등이 저장된다. |
| 특정 Git 이벤트(커밋 전, 푸시 후 등) 발생 시 자동으로 실행될 수 있는 클라이언트/서버 측 스크립트 디렉토리이다. |
|
|
| 각 브랜치와 HEAD의 참조 변경 이력을 기록하는 디렉토리이다. |
objects 디렉토리는 Git의 데이터 저장소 역할을 한다. 모든 파일 내용(Blob), 디렉토리 구조(Tree), 커밋 정보(Commit)는 이곳에 SHA-1 해시를 키로 하여 저장된다. 이 덕분에 Git은 내용을 기반으로 한 고유 식별을 수행하며, 데이터 무결성을 보장한다. refs 디렉토리와 HEAD 파일은 이러한 객체들에 대한 인간 친화적인 포인터 역할을 하여, 사용자가 긴 해시값 대신 main이나 v1.0 같은 이름으로 버전을 참조할 수 있게 한다.
Git의 내부 데이터 모델은 네 가지 주요 객체 유형(Blob, Tree, Commit, Tag)으로 구성됩니다. 이 객체들은 모두 SHA-1 해시를 고유 식별자로 사용하며, .git/objects 디렉토리에 저장됩니다.
Blob 객체는 파일의 내용을 저장하는 기본 단위입니다. 파일 이름이나 권한 정보는 포함하지 않고, 순수한 데이터만을 담습니다. 동일한 내용의 파일은 하나의 Blob 객체를 공유하여 공간을 절약합니다. Tree 객체는 디렉토리의 스냅샷에 해당하며, 해당 디렉토리에 포함된 Blob 객체(파일)와 다른 Tree 객체(하위 디렉토리)의 목록을 저장합니다. 각 항목은 파일명, 권한, 그리고 해당 객체의 SHA-1 해시를 포함합니다.
Commit 객체는 프로젝트 히스토리의 한 시점을 기록합니다. 이 객체는 최상위 Tree 객체의 해시(해당 커밋 시점의 워킹 디렉토리 상태), 부모 커밋의 해시(들), 작성자/커미터 정보, 그리고 커밋 메시지를 포함합니다. 이 구조는 커밋들이 연결된 방향성 비순환 그래프(DAG)를 형성하게 합니다. Annotated Tag 객체는 특정 커밋(또는 다른 객체)에 영구적인 이름을 붙일 때 사용되며, 태그 이름, 태그가 가리키는 객체의 해시, 태거 정보, 날짜, 그리고 메시지를 저장합니다.
이 객체 모델의 관계는 다음과 같이 요약할 수 있습니다.
객체 유형 | 저장 내용 | 역할 |
|---|---|---|
파일 데이터 | 파일 내용 자체 | |
디렉토리 목록(Blob/Tree 해시, 이름, 권한) | 디렉토리 구조 | |
최상위 Tree 해시, 부모 커밋 해시, 메타데이터, 메시지 | 프로젝트 히스토리의 한 단위 | |
대상 객체 해시, 메타데이터, 메시지 | 특정 커밋에 대한 영구 참조 |
모든 객체는 불변성을 가지며, 일단 생성되면 그 내용을 변경할 수 없습니다. 이 덕분에 Git은 데이터 무결성을 보장하고 효율적인 브랜치 생성 및 병합이 가능해집니다.
참조는 .git 디렉토리 내부의 refs/ 하위 디렉토리에 저장되며, 커밋 객체의 SHA-1 해시를 가리키는 간단한 포인터 역할을 한다. 참조의 주요 목적은 복잡한 해시 값을 기억하기 쉬운 이름(예: main, feature/login)으로 대체하여 사용자가 버전 관리를 편리하게 할 수 있도록 하는 것이다. 참조는 크게 브랜치, 태그, HEAD로 구분된다.
참조 유형 | 저장 위치 ( | 설명 | 특징 |
|---|---|---|---|
브랜치 |
| 개발 흐름을 나누는 이동 가능한 포인터 | 새로운 커밋이 생성되면 자동으로 최신 커밋을 가리키도록 이동한다. |
태그 |
| 특정 커밋에 부여하는 고정된 이름 (릴리스 버전 등) | |
원격 추적 브랜치 |
| 원격 저장소의 브랜치 상태를 마지막으로 가져온 시점의 커밋을 가리킨다. |
|
HEAD |
| 대부분 |
HEAD 파일은 일반적으로 ref: refs/heads/main과 같은 내용을 담고 있어, 현재 체크아웃된 브랜치를 간접적으로 참조한다. 이 구조 덕분에 git commit 명령을 실행하면, HEAD가 가리키는 브랜치 참조가 새로 생성된 커밋 객체의 해시로 자동 업데이트된다. 모든 참조는 git update-ref 명령으로 직접 수정할 수 있지만, 일반적으로는 git branch, git tag, git checkout 같은 상위 명령어를 통해 안전하게 관리한다.
커밋 생성 과정은 워킹 디렉토리, 스테이징 영역, 저장소라는 세 영역을 거친다. 사용자가 git add 명령을 실행하면, 변경된 파일의 내용을 기반으로 Blob 객체가 생성되고, 이 객체의 SHA-1 해시 값이 계산되어 .git/objects 디렉토리에 저장된다. 동시에 이 정보가 스테이징 영역(인덱스)에 기록된다. 이후 git commit 명령이 실행되면, 스테이징 영역의 상태를 담은 Tree 객체와 부모 커밋 해시, 작성자 정보, 커밋 메시지를 포함한 Commit 객체가 생성된다. 이 새로운 커밋 객체의 해시가 계산되고 저장되며, 현재 HEAD가 가리키는 브랜치 참조가 이 새로운 커밋을 가리키도록 갱신된다.
브랜치 생성은 단순히 .git/refs/heads/ 디렉토리 아래에 새로운 참조 파일을 만들고, 이를 특정 커밋 해시로 초기화하는 작업이다. 예를 들어 git branch feature 명령은 현재 HEAD가 가리키는 커밋 해시를 내용으로 하는 feature 파일을 생성한다. git checkout 명령은 지정한 브랜치나 커밋으로 워킹 디렉토리의 파일 상태를 변경하는 과정이다. 내부적으로는 해당 커밋이 가리키는 Tree 객체와 하위 Blob 객체들을 따라가서 파일들을 실제로 복원한다. 동시에 HEAD 참조가 새로 체크아웃한 브랜치를 가리키도록 업데이트된다.
병합(Merge)과 리베이스(Rebase)는 브랜치의 변경 이력을 통합하는 서로 다른 전략으로, 내부 동작이 근본적으로 다르다. git merge 명령은 두 브랜치의 공통 조상(Base Commit)과 각 브랜치의 최신 커밋을 비교하여 3-way merge 알고리즘을 수행한다. 충돌이 없으면 새로운 Merge Commit 객체를 생성하는데, 이 커밋은 두 개의 부모 커밋을 가진다. 반면 git rebase 명령은 현재 브랜치의 커밋들을 대상 브랜치의 최신 커밋 뒤로 순차적으로 재배치한다. 내부적으로는 재배치할 각 커밋의 변경사항에 대해 패치를 생성하고 이를 새로운 위치에 적용하여 본질적으로는 새로운 커밋 객체들을 생성한다. 결과적으로 히스토리가 선형적으로 보이게 되지만, 커밋의 SHA-1 해시 값이 변경된다[3].
커밋 생성 과정은 워킹 디렉토리에서 시작하여 스테이징 영역을 거쳐 최종적으로 저장소에 기록되는 일련의 단계를 거친다. 사용자가 git commit 명령을 실행하면, Git은 스테이징 영역(인덱스)에 등록된 스냅샷의 내용을 기반으로 새로운 커밋 객체를 생성하고 이를 영구적인 데이터베이스에 저장한다.
먼저, Git은 스테이징 영역에 등록된 파일들의 상태를 기준으로 트리 객체를 생성한다. 이 트리 객체는 프로젝트 디렉토리의 구조를 나타내며, 각 파일의 내용을 가리키는 블롭 객체에 대한 포인터와 파일명, 권한 정보를 포함한다. 이 트리 객체는 해당 시점의 프로젝트 전체 디렉토리 구조를 고유하게 표현하는 SHA-1 해시 값을 가진다.
다음으로, Git은 새로운 커밋 객체를 생성한다. 이 객체는 다음 정보를 포함한다.
* 방금 생성된 트리 객체의 해시 (스냅샷)
* 부모 커밋 객체의 해시 (직전 커밋을 가리킴)
* 커밋 작성자와 커밋터 정보 (이름, 이메일, 타임스탬프)
* 사용자가 입력한 커밋 메시지
이 과정이 완료되면, Git은 새로 생성된 커밋 객체의 고유 해시를 계산하고, 이를 .git/objects/ 디렉토리 내에 저장한다. 마지막으로, HEAD 참조가 가리키는 현재 브랜치 참조(예: .git/refs/heads/main)의 포인터를 새로운 커밋 객체의 해시로 업데이트한다. 이로써 커밋이 히스토리에 공식적으로 추가된다.
단계 | 설명 | 관련 Git 객체/영역 |
|---|---|---|
1. 파일 수정 | 워킹 디렉토리에서 파일을 추가, 수정, 삭제한다. | 워킹 디렉토리 |
2. 스테이징 |
| 스테이징 영역(인덱스), 블롭 객체 |
3. 트리 생성 | 스테이징 영역의 상태를 기반으로 디렉토리 구조를 나타내는 트리 객체를 생성한다. | |
4. 커밋 객체 생성 | 트리 해시, 부모 커밋 해시, 작성자 정보, 메시지를 포함한 커밋 객체를 생성하고 저장한다. | |
5. 참조 업데이트 | 현재 브랜치 참조(예: |
브랜치는 개발 흐름을 독립적으로 관리하기 위한 포인터이다. 기본적으로 마스터 브랜치(또는 메인 브랜치)에서 시작하며, 새로운 기능 개발이나 버그 수정을 위해 새로운 브랜치를 생성한다. 브랜치 생성은 git branch <브랜치명> 명령어로 수행된다. 이 명령은 새로운 참조(Refs)를 .git/refs/heads/ 디렉토리에 생성하지만, 현재 작업 중인 브랜치는 변경하지 않는다.
체크아웃은 특정 커밋이나 브랜치로 워킹 디렉토리의 상태를 전환하는 작업이다. git checkout <브랜치명> 명령을 실행하면, HEAD 참조가 해당 브랜치를 가리키도록 업데이트되고, 해당 브랜치가 최종적으로 가리키는 트리 객체의 내용이 워킹 디렉토리에 펼쳐진다. 새 브랜치를 생성하면서 동시에 체크아웃하려면 git checkout -b <새 브랜치명> 명령을 사용한다.
내부적으로 브랜치 생성과 체크아웃은 서로 다른 객체를 다룬다. 브랜치 생성은 단순히 새로운 참조(Refs)를 만드는 반면, 체크아웃은 HEAD의 상태를 변경하고 워킹 디렉토리의 파일들을 대체하는 복잡한 작업이다. 체크아웃 시 인덱스(스테이징 영역)도 새로운 브랜치의 상태로 리셋된다.
작업 | 명령어 | 주요 동작 | 결과 |
|---|---|---|---|
브랜치 생성 |
|
| 새 브랜치 존재, HEAD는 변하지 않음 |
브랜치 체크아웃 |
| HEAD가 | 작업 트리가 |
생성 및 체크아웃 |
| 위의 두 과정을 한 번에 수행 |
|
병합과 리베이스는 둘 다 브랜치의 변경 이력을 통합하는 Git의 핵심 기능이지만, 내부적으로 커밋 히스토리를 구성하는 방식이 근본적으로 다르다.
병합은 두 브랜치의 최종 상태를 결합하는 새로운 커밋 객체를 생성한다. 일반적으로 3-way 병합 알고리즘을 사용하며, 두 브랜치의 공통 조상 커밋(merge base), 현재 브랜치의 최신 커밋, 병합할 브랜치의 최신 커밋 이렇게 세 개의 스냅샷을 비교한다. Git은 이들을 분석하여 변경 사항을 통합하고, 성공적으로 병합되면 두 부모를 가진 새로운 "병합 커밋"을 생성한다. 이 방식은 기존 브랜치의 히스토리를 그대로 보존하며, 병합이 이루어진 시점과 맥락을 명확히 보여주는 장점이 있다.
반면, 리베이스는 현재 브랜치의 커밋들을 대상 브랜치의 최신 커밋 뒤로 재배치한다. 내부적으로는 현재 브랜치의 각 커밋을 순차적으로 임시 영역에 저장한 후, 대상 브랜치의 끝에서부터 차례대로 다시 적용한다. 이 과정에서 각 커밋의 SHA-1 해시는 새로운 부모 커밋을 바탕으로 재계산되어 완전히 새로운 커밋 객체로 생성된다. 결과적으로 히스토리가 마치 한 줄의 직선으로 발전한 것처럼 깔끔해지지만, 원본 커밋 객체들은 사라지고 그 내용을 복제한 새로운 객체들로 대체된다.
두 방식의 내부적 차이는 다음과 같이 정리할 수 있다.
특성 | 병합 (Merge) | 리베이스 (Rebase) |
|---|---|---|
히스토리 구조 | 분기와 병합을 보존하는 비선형 그래프 | 선형적이며 순차적인 히스토리 |
생성 객체 | 새로운 병합 커밋 객체 (부모 2개) | 기존 커밋들의 내용을 복제한 새로운 커밋 객체들 |
커밋 SHA-1 | 기존 커밋들의 해시 값 변경 없음 | 재배치된 커밋들은 새로운 해시 값을 가짐 |
내부 동작 | 3-way 병합을 통한 최종 상태 통합 | 커밋을 순차적으로 재적용(patch) |
따라서, 이미 공개된 저장소에 푸시된 커밋 히스토리를 리베이스로 재작성하는 것은 다른 협업자들의 히스토리와 불일치를 초래할 수 있어 일반적으로 권장되지 않는다. 반면, 병합은 히스토리의 정확한 기록을 보존하므로 공개 브랜치의 통합에 더 안전한 방법이다.
인터랙티브 리베이스는 리베이스 작업을 세밀하게 제어할 수 있는 강력한 기능이다. git rebase -i 명령을 사용하면 대상 커밋들의 목록을 편집기에서 확인하고, 순서를 변경하거나 커밋을 병합(squash), 수정(edit), 삭제(drop)하는 등의 작업을 수행할 수 있다. 이는 브랜치의 히스토리를 정리하거나 여러 개의 작은 커밋을 하나로 합칠 때 유용하다. 작업 중 특정 커밋에서 멈추어 내용을 수정한 후 계속 진행하는 것도 가능하다[4].
스태시는 현재 워킹 디렉토리와 스테이징 영역의 변경 사항을 임시로 저장하는 공간이다. git stash 명령을 실행하면 수정 중인 파일들을 잠시 보관해 두고 깨끗한 상태로 브랜치를 전환할 수 있다. 이후 git stash pop 또는 git stash apply로 저장한 작업을 다시 불러올 수 있다. 여러 번의 스태시는 스택 구조로 관리되며, git stash list로 목록을 확인하고 git stash drop으로 특정 스태시를 삭제할 수 있다.
후크(Hooks)는 Git이 특정 이벤트(예: 커밋 전, 푸시 후)를 실행할 때 자동으로 트리거되는 사용자 정의 스크립트다. .git/hooks/ 디렉토리에 위치하며, 셸 스크립트나 Perl, Python 등으로 작성할 수 있다. 대표적인 활용 예로는 커밋 메시지 형식 검사(commit-msg), 코드 스타일 검사(pre-commit), 서버 배포 자동화(post-receive) 등이 있다. 후크 스크립트는 프로젝트의 워크플로우를 표준화하고 자동화하는 데 기여한다.
후크 이름 | 실행 시점 | 주요 활용 예 |
|---|---|---|
| 커밋 메시지 작성 전 | 코드 린팅, 테스트 자동 실행 |
| 커밋 메시지 작성 후 | 메시지 형식(예: 이슈 번호 포함) 검증 |
| 커밋 완료 후 | 알림 전송 또는 로깅 |
| 원격 저장소로 푸시 전 | 통합 테스트 실행 |
| 원격 저장소에서 푸시 수신 후 | 자동 빌드 및 배포 서버 연동 |
인터랙티브 리베이스는 리베이스 작업을 수행할 때, 커밋 히스토리를 대화형으로 편집할 수 있는 강력한 기능이다. 일반적인 git rebase 명령이 브랜치의 커밋들을 새로운 베이스 위에 그대로 재적용하는 반면, 인터랙티브 리베이스는 재적용 과정에서 각 커밋을 수정, 병합, 삭제하거나 순서를 변경할 수 있다. 이는 깨끗하고 논리적인 프로젝트 히스토리를 만들기 위해 주로 사용된다.
명령은 git rebase -i <대상 브랜치 또는 커밋> 또는 git rebase -i HEAD~<숫자> 형태로 실행한다. 실행하면 텍스트 편집기에 재배치될 커밋들의 목록과 수행할 작업을 지정하는 명령어(pick, reword, edit, squash, fixup, drop 등)가 표시된다. 사용자는 이 목록을 편집하여 원하는 작업을 지시한 후 저장하고 편집기를 닫으면, Git이 지시에 따라 순차적으로 작업을 수행한다.
명령어 | 설명 |
|---|---|
pick | 커밋을 사용한다(기본 동작). |
reword | 커밋을 사용하지만, 커밋 메시지를 수정한다. |
edit | 커밋을 사용하지만, 커밋 자체를 수정할 수 있도록 중단한다. |
squash | 커밋을 사용하지만, 이전 커밋과 하나로 병합한다. |
fixup |
|
drop | 커밋을 제거한다. |
작업 중 edit 명령으로 지정된 커밋에 도달하면 Git은 작업을 일시 중지한다. 이때 사용자는 git commit --amend로 커밋 내용을 수정하거나 파일을 변경한 후 git rebase --continue를 실행하여 작업을 계속할 수 있다. 만약 문제가 발생하면 git rebase --abort를 실행하여 리베이스 시작 전 상태로 완전히 되돌릴 수 있다. 이 기능은 공유된 브랜치의 히스토리를 재작성할 경우 협업에 문제를 일으킬 수 있으므로, 일반적으로 자신의 로컬 브랜치나 아직 공개되지 않은 커밋에 대해 사용하는 것이 안전하다.
스태시는 워킹 디렉토리와 스테이징 영역의 변경 사항을 임시로 저장하는 Git의 기능이다. 현재 작업 중인 내용을 커밋하지 않고도 안전하게 보관해 두었다가, 나중에 다시 불러와 작업을 계속할 수 있게 해준다. 주로 다른 브랜치로 전환해야 할 때나, 현재 작업을 잠시 중단하고 급한 작업을 처리해야 할 때 유용하게 사용된다.
git stash 명령어를 실행하면, 추적 중인 파일의 수정 사항과 git add로 스테이징 영역에 올린 내용이 스택 형태의 저장 공간에 저장된다. 저장 후 워킹 디렉토리는 마지막 커밋 상태로 되돌아가 깨끗한 상태가 된다. 여러 번 스태시를 생성하면 스택에 순서대로 쌓이며, git stash list 명령으로 저장 목록을 확인할 수 있다. 저장된 스태시는 고유한 식별자(예: stash@{0})를 가지며, 각 항목에는 생성 당시의 브랜치와 커밋 메시지가 함께 기록된다.
가장 일반적인 활용 방법은 다음과 같다.
명령어 | 설명 |
|---|---|
| 변경 사항을 스태시에 저장한다. |
| 저장된 스태시 목록을 조회한다. |
| 지정된 스태시를 적용하되, 스택에서 제거하지 않는다. 이름을 생략하면 최신 스태시( |
| 지정된 스태시를 적용하고 스택에서 제거한다. |
| 지정된 스태시를 스택에서 삭제한다. |
| 모든 스태시를 삭제한다. |
git stash 명령은 -u 또는 --include-untracked 옵션과 함께 사용하여 추적되지 않는 새 파일까지 함께 저장할 수 있다. 또한 git stash save "메시지" 형식으로 저장 시 설명을 추가할 수 있으나, save 명령은 deprecated 되었으며 git stash push -m "메시지"를 사용하는 것이 권장된다. 스태시를 적용할 때 워킹 디렉토리에 아직 커밋되지 않은 변경 사항이 있다면 충돌이 발생할 수 있으므로 주의가 필요하다.
Git 후크는 특정 이벤트가 발생했을 때 자동으로 실행되는 사용자 정의 스크립트이다. 이 스크립트들은 .git/hooks 디렉토리에 위치하며, 커밋이나 푸시와 같은 중요한 작업의 전후에 특정 작업을 수행하도록 설정할 수 있다. 후크는 주로 워크플로우를 자동화하거나, 정책을 강제하며, 통합을 용이하게 하는 데 사용된다.
Git은 클라이언트 측 후크와 서버 측 후크로 구분된다. 클라이언트 측 후크는 개발자의 로컬 작업에 반응하며, pre-commit, prepare-commit-msg, commit-msg, post-commit 등이 대표적이다. 예를 들어, pre-commit 후크는 스테이징 영역의 코드에 대해 린트 검사나 테스트를 실행하여 품질 기준을 통과하지 못하면 커밋을 중단시킬 수 있다. 서버 측 후크는 원격 저장소에 푸시가 발생했을 때 실행되며, pre-receive와 post-receive가 있다. pre-receive 후크는 푸시된 내용을 검사하여 특정 규칙(예: 커밋 메시지 형식, 병합 커밋 금지)을 위반하면 푸시 자체를 거부할 수 있다.
후크 스크립트는 기본적으로 샘플 파일(.sample 확장자)로 제공된다. 이를 활성화하려면 파일명에서 .sample 확장자를 제거하고 실행 권한을 부여하면 된다. 후크는 셸 스크립트, Perl, Python, Ruby 등 다양한 언어로 작성할 수 있다. 또한, 프로젝트 팀 전체에 일관된 후크를 적용하기 위해 템플릿 디렉토리(init.templatedir 설정)를 사용하거나, Husky와 같은 타사 도구를 활용하여 관리하는 것이 일반적이다.
후크 이름 | 실행 시점 | 주요 용도 |
|---|---|---|
|
| 코드 스타일 검사, 테스트 실행, 불필요한 파일(로그, 빌드 산출물) 커밋 방지 |
| 커밋 메시지가 임시 파일에 작성된 후 | 커밋 메시지 형식(예: 이슈 번호 포함) 검증 |
|
| 푸시 전 추가 테스트 실행 또는 특정 브랜치 푸시 제한 |
|
| 작업 디렉토리 의존성 자동 설치 또는 환경 설정 |
| 서버가 푸시된 참조를 받은 후, 실제 적용 전 | 푸시된 모든 커밋에 대한 중앙 집중형 정책 검사 |
커스터마이징 측면에서는 Git의 설정 시스템을 활용할 수 있다. git config 명령어를 통해 사용자별 또는 프로젝트별로 별칭(Alias)을 설정하여 긴 명령어를 단축하거나, 기본 동작을 변경할 수 있다. 예를 들어, git config --global alias.st status는 git st로 상태를 확인할 수 있게 한다. 또한, core.editor, merge.tool, color.ui와 같은 설정을 통해 Git의 사용자 인터페이스와 도구 통합을 개인에 맞게 조정할 수 있다.
Git을 사용하다 보면 커밋 메시지를 잘못 작성하거나, 실수로 커밋을 삭제하거나, 병합 충돌을 마주치는 상황이 발생할 수 있다. 이러한 문제들을 효과적으로 해결하는 방법을 이해하는 것은 중요하다.
커밋 메시지를 수정하는 가장 일반적인 방법은 git commit --amend 명령어를 사용하는 것이다. 이 명령은 가장 최신의 커밋 하나를 수정할 수 있으며, 메시지 변경뿐만 아니라 스테이징 영역에 새로 추가된 변경 사항을 해당 커밋에 포함시키는 데에도 사용된다. 이미 원격 저장소에 푸시된 커밋의 메시지를 수정하는 것은 협업 중인 다른 사용자의 히스토리를 엉망으로 만들 수 있으므로 주의가 필요하다. 여러 커밋을 수정하거나 재정렬해야 할 경우 인터랙티브 리베이스를 사용한다.
분실한 커밋을 복구하기 위해서는 먼저 git reflog 명령을 사용하여 HEAD와 브랜치 참조가 가리켜왔던 모든 커밋의 기록을 확인한다. reflog에서 사라진 커밋의 해시를 찾았다면, 해당 해시를 사용해 새 브랜치를 생성(git branch recovery-branch <해시>)하여 커밋을 복구할 수 있다. 객체 데이터베이스에 저장된 커밋은 가비지 컬렉션(git gc)이 실행되기 전까지는 물리적으로 삭제되지 않는다는 점이 복구 가능성을 제공한다.
병합 충돌은 두 브랜치에서 동일한 파일의 동일한 부분을 수정했을 때 발생한다. 충돌이 발생하면 Git은 해당 파일에 충돌 마커(<<<<<<<, =======, >>>>>>>)를 삽입한다. 해결 과정은 이 마커들을 수동으로 제거하고 원하는 코드 버전으로 파일을 편집한 후, 수정된 파일을 스테이징 영역에 추가(git add)하고 병합을 완료(git commit)하는 것이다. 복잡한 충돌의 경우 비교 도구(diff tool)를 사용하면 시각적으로 해결하는 데 도움이 된다. 충돌 해결을 중단하고 병합 전 상태로 되돌리려면 git merge --abort 명령을 사용한다.
문제 상황 | 주요 해결 명령어 | 주의사항 |
|---|---|---|
최신 커밋 메시지/내용 수정 |
| 원격에 푸시된 커밋 수정 시 협업자와 사전 조율 필요 |
과거 여러 커밋 수정 |
| 공용 브랜치 히스토리 재작성 시 주의 |
분실한 커밋 찾기 및 복구 |
| 가비지 컬렉션 실행 주기 확인 |
병합 충돌 해결 | 파일 수동 편집 후 | 충돌 마커를 완전히 제거해야 함 |
병합 중단 및 되돌리기 |
| 병합 시작 전 상태로 워킹 디렉토리가 복원됨 |
커밋 메시지를 수정하는 작업은 이미 생성된 커밋의 기록을 변경하는 행위로, 주로 메시지 오타를 수정하거나 설명을 명확히 할 때 사용합니다. 가장 일반적인 방법은 가장 최근 커밋의 메시지를 수정하는 git commit --amend 명령어입니다. 이 명령을 실행하면 스테이징 영역에 새로 준비된 변경 사항이 있다면 함께 최근 커밋에 추가되며, 편집기가 열려 커밋 메시지를 수정할 수 있습니다.
이미 리모트 저장소에 푸시된 커밋의 메시지를 수정하는 것은 협업 중인 다른 개발자의 히스토리에 영향을 줄 수 있으므로 주의가 필요합니다. 로컬에서 수정 후 강제 푸시(git push --force)를 해야 하지만, 이는 공유된 브랜치에서 사용을 피해야 합니다. 대신, 새로운 커밋으로 오류를 정정하는 방법을 고려하는 것이 안전합니다.
과거의 특정 커밋 메시지를 수정하려면 인터랙티브 리베이스를 사용합니다. git rebase -i HEAD~n 명령으로 대화형 리베이스를 시작한 후, 수정하려는 커밋 앞의 pick을 reword 또는 r로 변경합니다. 이후 단계에서 해당 커밋의 메시지를 새로 작성할 수 있습니다. 이 방법은 커밋 해시를 변경하므로, 아직 푸시하지 않은 로컬 브랜치에서 수행해야 합니다.
상황 | 권장 명령어 | 주의사항 |
|---|---|---|
최근 커밋 메시지 수정 |
| 푸시 전에만 사용 |
과거 커밋 메시지 수정 |
| 공유 브랜치에서 사용 자제 |
이미 푸시된 커밋 수정 |
| 협업자와 사전 조율 필수 |
커밋 메시지 수정은 Git 히스토리를 재작성하는 행위입니다. 따라서 개인 브랜치나 푸시되지 않은 작업에서는 자유롭게 사용할 수 있지만, 공용 브랜치(main, develop 등)에서는 팀의 규칙을 확인하고 신중하게 적용해야 합니다.
Git에서 작업 내역을 잃어버리는 것은 일반적으로 커밋이 참조에서 벗어났거나, 브랜치가 삭제되었을 때 발생합니다. Git은 데이터를 실제로 삭제하지 않고, 더 이상 접근할 수 없는 상태로 만듭니다. 이러한 "분실된" 커밋은 Git 내부 명령어를 통해 복구할 수 있습니다.
가장 일반적인 복구 방법은 git reflog 명령어를 사용하는 것입니다. reflog는 HEAD와 브랜치 참조가 가리키는 대상의 모든 변경 이력을 일정 기간 동안 보관합니다. 잘못된 리셋이나 브랜치 삭제 후에도, 해당 커밋의 해시가 reflog에 남아 있다면 아래와 같이 복원할 수 있습니다.
```bash
git reflog
# 출력에서 복구하려는 커밋 해시 확인 (예: abc1234)
git checkout -b recovered-branch abc1234
```
만약 reflog 기록에서도 사라졌다면, 객체 데이터베이스를 직접 탐색해야 합니다. git fsck --lost-found 명령어는 연결이 끊어진 모든 객체를 찾아서 .git/lost-found 디렉토리에 나열합니다. 여기서 찾은 커밋 해시를 확인하여 새 브랜치를 생성하면 복구가 완료됩니다.
복구 방법 | 사용 명령어 | 적용 시나리오 | 주의사항 |
|---|---|---|---|
reflog 사용 |
| 최근 작업 내역 실수로 삭제 | reflog는 일정 시간 후 소멸됨 |
객체 데이터베이스 탐색 |
| reflog에서도 삭제된 오래된 커밋 | 해시를 직접 식별해야 함 |
ORIG_HEAD 참조 이용 |
|
| 일부 명령어만 ORIG_HEAD를 생성함 |
복구 성공 가능성을 높이기 위해서는 정기적인 원격 저장소 푸시가 필수적입니다. 원격에 백업된 커밋은 언제든지 git fetch와 git checkout을 통해 쉽게 되살릴 수 있습니다. 또한, 중요한 변경 사항을 병합하거나 리셋하기 전에는 임시 브랜치를 만들어 작업 내역을 보존하는 것이 안전합니다.
병합 또는 리베이스 작업 중 동일한 파일의 동일한 부분을 서로 다른 방식으로 수정한 경우 Git은 자동으로 병합을 수행할 수 없고 충돌을 발생시킨다. 충돌이 발생하면 Git은 해당 파일에 충돌 마커를 삽입하고, 작업자는 이를 수동으로 해결한 후 병합을 완료해야 한다.
충돌을 해결하는 일반적인 전략은 다음과 같다.
1. git status 명령어로 충돌이 발생한 파일을 확인한다.
2. 충돌이 표시된 파일을 편집기로 열어 <<<<<<<, =======, >>>>>>>로 표시된 충돌 마커 사이의 내용을 원하는 최종 코드로 수정한다.
3. 충돌 마커를 모두 제거하고 파일을 저장한다.
4. 수정이 완료된 파일을 git add <파일명> 명령어로 스테이징 영역에 추가한다.
5. 모든 충돌이 해결되면 git commit 명령어로 병합 커밋을 완성한다.
충돌 해결 시 고려할 수 있는 구체적인 접근법은 아래와 같다.
접근법 | 설명 | 사용 시나리오 |
|---|---|---|
현재 브랜치 변경사항 유지 |
| 다른 브랜치의 변경을 무시하고 자신의 작업을 우선시할 때 |
다른 브랜치 변경사항 수용 |
| 자신의 변경을 포기하고 상대방의 변경을 그대로 적용할 때 |
수동 통합 | 두 변경사항을 모두 참고하여 완전히 새로운 코드를 작성한다. | 두 변경이 모두 필요하거나 서로 융합되어야 할 때 |
외부 병합 도구 활용 |
| 복잡한 충돌을 그래픽 인터페이스로 해결하려 할 때 |
충돌을 예방하거나 관리하기 위한 전략도 중요하다. 작고 빈번한 커밋을 생성하면 변경 범위가 좁아져 충돌 가능성이 줄어든다. 기능별로 토픽 브랜치를 분리하고, 주기적으로 git fetch와 git merge를 수행하여 기준 브랜치의 변경사항을 자신의 브랜치에 통합하면, 나중에 발생할 수 있는 대규모 충돌을 사전에 분산시킬 수 있다. 또한, 팀 내 코딩 컨벤션과 담당 영역을 명확히 하는 것도 근본적인 충돌 예방에 도움이 된다.