Commit: 변경 이력을 스냅샷으로 기록하는 Git의 기본 단위
커밋(commit)은 Git이 프로젝트 전체의 특정 시점 상태를 스냅샷(snapshot)으로 저장한 기록이다. 각 커밋은 SHA-1(또는 SHA-256) 해시로 식별되고 부모 커밋을 가리켜 이력 그래프를 만든다. 스테이징 영역(index)을 거쳐 커밋하는 흐름과 좋은 커밋 단위를 잡는 법까지 다룬다.
By 송민성
1. 개념
커밋(commit)은 Git 저장소에 기록되는 하나의 버전이다. 커밋 하나에는 다음 정보가 담긴다.
- 프로젝트 루트 디렉터리의 스냅샷을 가리키는 트리(tree) 객체
- 부모(parent) 커밋의 해시 (최초 커밋은 부모가 없다)
- 작성자(author), 커밋터(committer), 시각
- 커밋 메시지(message)
커밋은 파일별 변경분(diff)이 아니라 그 시점 전체 상태의 스냅샷을 가리킨다. 변경되지 않은 파일은 기존 객체를 재사용하므로 저장 공간을 중복해서 쓰지 않는다. diff는 두 커밋을 비교할 때 계산해서 보여주는 값이다.
2. 왜 사용하는가
- 이력 추적: 누가, 언제, 왜 코드를 바꿨는지 남는다.
- 되돌리기: 문제가 생기면 이전 커밋 상태로 돌아가거나 특정 커밋의 변경만 취소할 수 있다.
- 협업: 브랜치(branch), 병합(merge), 리베이스(rebase)가 모두 커밋 그래프 위에서 동작한다.
- 원인 분석:
git bisect,git blame,git log는 커밋 단위로 버그 도입 시점을 찾는다.
3. 동작 원리
Git은 파일이 존재하는 위치를 세 영역으로 나눈다.
- 작업 디렉터리(working directory): 실제로 편집하는 파일
- 스테이징 영역(staging area, index): 다음 커밋에 포함할 변경을 모아두는 곳
- 저장소(repository,
.git): 커밋된 객체가 저장되는 곳
git add는 변경을 스테이징 영역에 올리고, git commit은 스테이징 영역의 내용을 기준으로 새 커밋을 만든다. 내부적으로는 다음 객체가 생성된다.
- 블롭(blob): 파일 내용
- 트리(tree): 디렉터리 구조와 블롭 목록
- 커밋(commit): 트리 + 부모 + 메타데이터
각 객체는 내용으로 계산한 해시가 이름이 된다(내용 주소 지정, content-addressable). 커밋 해시에는 부모 해시가 포함되므로 과거 커밋을 바꾸면 이후 모든 커밋의 해시가 달라진다. 브랜치는 특정 커밋을 가리키는 포인터일 뿐이며, 새 커밋을 만들면 현재 브랜치 포인터가 새 커밋으로 이동한다. HEAD는 현재 체크아웃한 브랜치(또는 커밋)를 가리킨다.
4. 코드 예제
임시 저장소를 만들어 커밋 과정을 직접 확인한다.
mkdir demo-commit && cd demo-commit
git init
git config user.name "Demo"
git config user.email "demo@example.com"
# 1) 첫 번째 커밋
echo "hello" > a.txt
git add a.txt
git commit -m "feat: add a.txt"
# 2) 파일 수정 후 일부만 스테이징
echo "world" >> a.txt
echo "memo" > b.txt
git add a.txt # b.txt는 스테이징하지 않음
git status --short # M a.txt / ?? b.txt
git commit -m "feat: append world to a.txt"
# 3) 이력과 객체 확인
git log --oneline --graph
git cat-file -p HEAD # tree, parent, author, message 확인
git show --stat HEAD # 이 커밋에서 바뀐 파일 요약
# 4) 직전 커밋 메시지 수정 (아직 공유하지 않은 커밋에 한함)
git commit --amend -m "feat: append world line to a.txt"
# 5) 이전 커밋을 취소하는 새 커밋 생성 (이력 보존)
git revert HEAD --no-editgit cat-file -p HEAD 출력에는 tree, parent, author, committer 줄과 메시지가 나타난다. 두 번째 커밋에서는 parent가 첫 커밋 해시를 가리킨다.
5. 시간 복잡도 또는 성능 특성
- 커밋은 변경된 파일의 블롭과 영향받는 트리 객체만 새로 만든다. 변경되지 않은 하위 트리와 블롭은 기존 객체를 그대로 참조한다.
- 객체는 압축(zlib)되어 저장되고, 이후
git gc나 팩 파일(packfile)로 델타 압축되어 저장 공간이 줄어든다. git log는 부모 포인터를 따라가며 이력을 순회하므로, 출력하는 커밋 수에 비례해 비용이 든다.- 이력이 매우 큰 저장소에서도 로컬 커밋 생성 자체는 네트워크가 필요 없어 빠르다. 커밋은 로컬 연산이고,
git push를 해야 원격 저장소에 반영된다.
6. 실무 사용 사례
- 작은 단위의 커밋: 하나의 커밋에는 하나의 논리적 변경만 담는다. 리뷰, 되돌리기,
bisect가 쉬워진다. - 커밋 메시지 규칙: Conventional Commits(
feat:,fix:,docs:,refactor:)를 쓰면 변경 로그 자동 생성과 버전 관리에 활용할 수 있다. - 부분 스테이징:
git add -p로 한 파일 안의 변경을 나눠 서로 다른 커밋으로 기록한다. - 안전한 취소: 이미 공유된 커밋은
git revert로 취소하고, 로컬에서만 존재하는 커밋은git reset이나--amend로 정리한다. - CI 연계: 푸시된 커밋 단위로 테스트와 배포 파이프라인이 실행된다.
7. 주의할 점
- 이미 푸시한 커밋을 amend/rebase하지 않는다. 해시가 바뀌므로 다른 사람의 이력과 충돌한다. 불가피하면 팀과 합의하고
--force-with-lease를 쓴다. - 비밀 정보 커밋 금지: API 키나 비밀번호가 한 번 커밋되면 이후 파일을 지워도 이력에 남는다. 키를 폐기(rotate)하는 것이 우선이다.
- `git add .` 남용: 빌드 산출물,
.env, 로그 파일이 섞이기 쉽다..gitignore를 먼저 설정하고git status로 확인한다. - 의미 없는 메시지:
fix,update,asdf같은 메시지는 나중에 이력을 읽을 때 쓸모가 없다. 무엇을 왜 바꿨는지 쓴다. - 여러 변경을 한 커밋에 묶기: 기능 추가와 포맷팅, 리팩터링이 섞이면 되돌리기와 리뷰가 어려워진다.
- 작성자 정보 확인:
user.name,user.email이 올바르게 설정되어 있는지 확인한다. 설정이 없으면 의도하지 않은 정보로 기록될 수 있다.
8. 핵심 정리
- 커밋은 특정 시점 프로젝트 전체의 스냅샷을 가리키며, 트리·부모·작성자 정보·메시지로 구성된다.
git add로 스테이징 영역에 올린 내용이git commit으로 저장소에 기록된다.- 커밋은 해시로 식별되고 부모를 참조하는 그래프를 이루며, 과거 커밋을 바꾸면 이후 해시가 모두 바뀐다.
- 커밋은 로컬 연산이며 원격에 반영하려면
push가 필요하다. - 논리적 단위로 작게 나누고, 의미 있는 메시지를 쓰며, 공유한 이력은
revert로 되돌린다.