고급 Git: 내부 원리와 고급 명령어
Git 객체 모델, rebase/cherry-pick/bisect 등 고급 명령어와 내부 동작 원리
개요
Git을 효과적으로 활용하려면 표면적인 명령어 사용을 넘어 내부 데이터 구조와 동작 원리를 이해해야 한다. 이 노트에서는 Git의 내부 객체 모델부터 고급 명령어, 그리고 모노레포 시대의 최적화 기법까지 다룬다.
Git 내부 객체 모델
Git은 본질적으로 Content-Addressable 파일 시스템이다. 모든 데이터는 4가지 객체 타입으로 저장된다.
4가지 객체 타입
┌──────────────────────────────────────────────┐
│ Commit │
│ tree: abc123... │
│ parent: def456... │
│ author: jk <jk@email.com> 1707523200 +0900 │
│ message: "feat: add payment module" │
└──────────────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Tree │
│ blob a1b2c3 README.md │
│ blob d4e5f6 package.json │
│ tree 789abc src/ │
└──────┬────────────────────┬──────────────────┘
│ │
▼ ▼
┌─────────────┐ ┌────────────────────┐
│ Blob │ │ Tree │
│ (파일 내용) │ │ blob ff00aa app.ts│
│ │ │ blob bb11cc db.ts │
└─────────────┘ └────────────────────┘| 객체 타입 | 설명 | 저장 내용 |
|---|---|---|
| Blob | 파일의 내용 (이름 없음) | 파일 데이터 그 자체 |
| Tree | 디렉토리 구조 | 파일명 + Blob/Tree 참조 |
| Commit | 스냅샷의 메타데이터 | Tree 참조, 부모 커밋, 작성자, 메시지 |
| Tag | 주석이 달린 태그 | Commit 참조, 태그 메시지 |
직접 확인하기
# 커밋 객체 내용 보기
git cat-file -p HEAD
# 트리 객체 내용 보기
git cat-file -p HEAD^{tree}
# 블롭 객체 내용 보기 (특정 파일)
git cat-file -p HEAD:src/app.ts
# 객체 타입 확인
git cat-file -t abc1234References (참조)
Git의 브랜치와 태그는 **커밋을 가리키는 포인터(Reference)**에 불과하다:
.git/refs/
├── heads/ # 로컬 브랜치
│ ├── main # → commit hash
│ └── feature/auth # → commit hash
├── tags/ # 태그
│ └── v1.0.0 # → commit hash (or tag object)
└── remotes/ # 원격 추적 브랜치
└── origin/
└── main # → commit hash- HEAD: 현재 체크아웃된 브랜치를 가리키는 심볼릭 참조
- Detached HEAD: HEAD가 브랜치가 아닌 특정 커밋을 직접 가리키는 상태
# HEAD 확인
cat .git/HEAD
# ref: refs/heads/main
# 브랜치가 가리키는 커밋 확인
cat .git/refs/heads/main
# a1b2c3d4e5f6...Rebase vs Merge
Merge (병합)
main: A ─── B ─── C ──────── M (merge commit)
\ /
feature: D ─── E- 보존: 모든 브랜치 히스토리가 그대로 보존됨
- 3-way merge: 공통 조상(C), 두 브랜치의 최신 커밋으로 병합
- 머지 커밋(M)이 생성됨
Rebase (리베이스)
main: A ─── B ─── C
\
feature: D' ─── E' (새 커밋 해시)- 재작성: feature 브랜치의 커밋들이 main 위에 새로 적용됨
- 깨끗한 히스토리: 선형(Linear) 히스토리 유지
- 커밋 해시가 변경됨 (새 커밋 생성)
Interactive Rebase (rebase -i)
git rebase -i HEAD~5pick abc1234 feat: add user model
squash def5678 fix: typo in user model # 이전 커밋과 합침
pick ghi9012 feat: add user repository
reword jkl3456 feat: add user service # 메시지만 수정
drop mno7890 WIP: debugging # 커밋 삭제| 명령어 | 동작 |
|---|---|
pick | 커밋을 그대로 사용 |
reword | 커밋 메시지만 수정 |
squash | 이전 커밋과 합치고 메시지도 합침 |
fixup | 이전 커밋과 합치되 메시지는 버림 |
drop | 커밋 삭제 |
edit | 커밋을 수정 (amend) |
언제 무엇을 사용할 것인가?
| 상황 | 권장 전략 |
|---|---|
| 공유 브랜치 (main, develop) | Merge -- 히스토리 보존 |
| 개인 feature 브랜치 정리 | Rebase -- 깨끗한 히스토리 |
| PR 전 커밋 정리 | Interactive Rebase -- squash, fixup |
| 이미 push된 커밋 | Merge -- rebase 금지 (force push 위험) |
황금 규칙: 이미 공유된(push된) 커밋은 절대 rebase하지 않는다.
Cherry-pick
정의
특정 커밋을 선택하여 현재 브랜치에 적용하는 명령어이다. 커밋의 변경 사항만 가져온다.
사용법
# 단일 커밋 cherry-pick
git cherry-pick abc1234
# 여러 커밋 cherry-pick
git cherry-pick abc1234 def5678
# 범위 cherry-pick (abc1234 미포함, def5678 포함)
git cherry-pick abc1234..def5678
# 커밋하지 않고 변경사항만 스테이징
git cherry-pick --no-commit abc1234활용 사례
- 핫픽스 백포트: production 브랜치의 수정을 develop에도 적용
- 기능 선별: feature 브랜치의 일부 커밋만 릴리스에 포함
- 실수 복구: 잘못된 브랜치에 커밋한 내용을 올바른 브랜치로 이동
주의사항
- Cherry-pick은 새 커밋을 생성한다 (다른 해시)
- 같은 변경이 두 브랜치에 존재하면 나중에 머지 시 충돌 가능
- 남용하면 히스토리 추적이 어려워짐
Bisect (이진 탐색)
정의
**이진 탐색(Binary Search)**을 사용하여 버그를 도입한 정확한 커밋을 찾는 명령어이다.
사용법
# bisect 시작
git bisect start
# 현재 버전이 버그가 있음을 표시
git bisect bad
# 알려진 정상 버전을 표시
git bisect good v1.0.0
# Git이 중간 커밋으로 자동 체크아웃
# 테스트 후 결과 입력
git bisect good # 이 커밋은 정상
# 또는
git bisect bad # 이 커밋은 버그 있음
# Git이 다시 중간 커밋으로 체크아웃...
# 반복하면 O(log n)으로 버그 커밋 특정
# bisect 종료
git bisect reset자동화된 bisect
# 테스트 스크립트로 자동 bisect
# 스크립트 종료 코드: 0 = good, 1 = bad
git bisect start HEAD v1.0.0
git bisect run ./test-for-bug.sh효율성
1000개의 커밋이 있어도 약 10번의 테스트로 문제 커밋을 특정할 수 있다 (log2(1000) ≈ 10).
Stash (임시 저장)
정의
작업 중인 변경사항을 임시로 저장하고, 깨끗한 워킹 디렉토리로 돌아가는 명령어이다.
주요 사용법
# 현재 변경사항 임시 저장
git stash
# 메시지와 함께 저장
git stash save "WIP: payment integration"
# untracked 파일도 포함하여 저장
git stash -u
# stash 목록 확인
git stash list
# stash@{0}: On feature/auth: WIP: payment integration
# stash@{1}: On main: debugging session
# 가장 최근 stash 적용 (stash 유지)
git stash apply
# 가장 최근 stash 적용 후 삭제
git stash pop
# 특정 stash 적용
git stash apply stash@{2}
# stash를 새 브랜치로 적용
git stash branch new-branch-name
# stash 내용 확인
git stash show -p stash@{0}
# 특정 stash 삭제
git stash drop stash@{0}
# 모든 stash 삭제
git stash clear활용 사례
- 긴급 핫픽스를 위해 작업 중인 내용 임시 저장
- 브랜치 전환 전 커밋하지 않은 변경사항 보관
- 실험적 변경사항을 나중에 다시 적용하기 위해 보관
Reflog (참조 로그)
정의
HEAD와 브랜치 참조의 모든 변경 이력을 기록하는 안전망(Safety Net)이다. 실수로 삭제하거나 리셋한 커밋을 복구할 수 있다.
사용법
# HEAD 이동 이력 확인
git reflog
# a1b2c3d HEAD@{0}: commit: feat: add payment
# d4e5f6g HEAD@{1}: checkout: moving from main to feature
# h7i8j9k HEAD@{2}: reset: moving to HEAD~3
# l0m1n2o HEAD@{3}: commit: feat: add user model
# 특정 브랜치의 reflog
git reflog show feature/auth
# 삭제된 커밋 복구
git checkout HEAD@{3}
# 또는
git branch recovered-branch HEAD@{3}
# 잘못된 reset 되돌리기
git reset --hard HEAD@{2}주의사항
- Reflog는 로컬에만 존재 (push되지 않음)
- 기본적으로 90일간 보관 (gc로 정리됨)
git gc --prune=now는 reflog도 정리하므로 주의
Git Worktree (워크트리)
정의
하나의 Git 리포지토리에서 여러 워킹 디렉토리를 동시에 사용할 수 있게 하는 기능이다. 브랜치 전환 없이 여러 브랜치를 동시에 작업할 수 있다.
사용법
# 새 워크트리 생성 (기존 브랜치)
git worktree add ../project-hotfix hotfix/payment-bug
# 새 워크트리 + 새 브랜치 생성
git worktree add -b feature/new-api ../project-new-api
# 워크트리 목록 확인
git worktree list
# /Users/jk/project abc1234 [main]
# /Users/jk/project-hotfix def5678 [hotfix/payment-bug]
# /Users/jk/project-new-api ghi9012 [feature/new-api]
# 워크트리 제거
git worktree remove ../project-hotfix
# 스테일 워크트리 정리
git worktree prune활용 사례
- 핫픽스 병행: main에서 작업 중 긴급 핫픽스를 다른 디렉토리에서 처리
- 코드 리뷰: 리뷰 대상 PR을 별도 워크트리에서 체크아웃하여 검토
- 빌드 비교: 두 버전을 동시에 빌드하여 성능 비교
- 모노레포: 다른 패키지의 브랜치를 독립적으로 작업
Worktree vs 브랜치 전환
| 비교 | 브랜치 전환 (checkout) | Worktree |
|---|---|---|
| 워킹 디렉토리 | 하나 | 여러 개 |
| stash 필요 | 예 (변경사항이 있으면) | 아니오 |
| 빌드 캐시 | 무효화됨 | 독립적으로 유지 |
| 동시 작업 | 불가 | 가능 |
Git LFS (Large File Storage)
정의
대용량 파일(바이너리, 미디어, 데이터셋)을 Git 리포지토리 외부 서버에 저장하고, 포인터 파일만 Git에 커밋하는 확장 기능이다.
설정 및 사용법
# Git LFS 설치 및 초기화
git lfs install
# 대용량 파일 패턴 추적
git lfs track "*.psd"
git lfs track "*.zip"
git lfs track "data/**"
# .gitattributes에 패턴이 기록됨
cat .gitattributes
# *.psd filter=lfs diff=lfs merge=lfs -text
# *.zip filter=lfs diff=lfs merge=lfs -text
# .gitattributes를 커밋
git add .gitattributes
git commit -m "chore: configure Git LFS"
# 이후 일반적으로 add/commit/push하면 자동으로 LFS 처리
git add design.psd
git commit -m "feat: add design asset"
git push동작 원리
리포지토리 (.git) LFS 서버 (외부)
┌─────────────────┐ ┌──────────────────┐
│ 포인터 파일 │ │ 실제 파일 내용 │
│ (SHA-256 해시, │ ──→ │ design.psd │
│ 크기 등 메타데이터)│ │ (원본 바이너리) │
└─────────────────┘ └──────────────────┘git filter-repo (히스토리 재작성)
정의
Git 리포지토리의 전체 히스토리를 재작성하는 도구이다. git filter-branch의 빠르고 안전한 대체제이다.
사용 사례
# 특정 파일의 전체 히스토리 삭제 (실수로 커밋된 비밀 키 등)
git filter-repo --path secrets.env --invert-paths
# 특정 디렉토리만 남기고 나머지 삭제 (모노레포 분리)
git filter-repo --path src/auth-service/ --path-rename src/auth-service/:
# 이메일 주소 일괄 변경
git filter-repo --email-callback '
return email.replace(b"old@company.com", b"new@company.com")
'
# 대용량 파일 식별
git filter-repo --analyze주의사항
- 모든 커밋 해시가 변경됨 (히스토리 재작성)
- 팀원 전원이 force-clone 해야 함
- 백업 후 실행할 것
Packfile과 Delta 압축
Git의 저장 효율화
Git은 초기에 각 객체를 **느슨한 객체(Loose Object)**로 개별 저장하지만, 일정 조건에서 **팩파일(Packfile)**로 압축한다.
팩파일 동작 원리
느슨한 객체들 (초기)
.git/objects/
├── a1/b2c3d4... (blob)
├── d4/e5f6g7... (blob)
└── ...
팩파일 (압축 후)
.git/objects/pack/
├── pack-abc123.pack (객체 데이터)
└── pack-abc123.idx (인덱스 -- 빠른 탐색)Delta 압축
- 유사한 객체 간의 **차이(Delta)**만 저장
- 큰 파일이 약간만 변경되어도 전체를 다시 저장하지 않음
git gc가 팩파일 생성 및 최적화 수행
# 수동 팩파일 생성
git gc
# 리포지토리 크기 확인
git count-objects -vH
# 팩파일 통계
git verify-pack -v .git/objects/pack/pack-*.idx | head -20모노레포 최적화
Shallow Clone (얕은 클론)
# 최근 1개 커밋만 클론 (히스토리 불필요 시)
git clone --depth 1 https://github.com/org/monorepo.git
# 필요 시 히스토리 추가 fetch
git fetch --unshallowSparse Checkout (부분 체크아웃)
# sparse-checkout 초기화
git sparse-checkout init --cone
# 필요한 디렉토리만 체크아웃
git sparse-checkout set services/auth-service shared/common
# 추가
git sparse-checkout add services/payment-service
# 현재 설정 확인
git sparse-checkout listPartial Clone (부분 클론)
# 블롭 없이 클론 (필요할 때 자동 다운로드)
git clone --filter=blob:none https://github.com/org/monorepo.git
# 트리도 제외하여 최소한으로 클론
git clone --filter=tree:0 https://github.com/org/monorepo.git모노레포 최적화 조합
# 최적 조합: 부분 클론 + Sparse Checkout
git clone --filter=blob:none --sparse https://github.com/org/monorepo.git
cd monorepo
git sparse-checkout set services/my-service shared/utils| 기법 | 효과 | 사용 시나리오 |
|---|---|---|
| Shallow Clone | 히스토리 축소 | CI/CD, 일회성 빌드 |
| Sparse Checkout | 워킹 디렉토리 축소 | 모노레포에서 특정 서비스만 작업 |
| Partial Clone | 초기 클론 크기 축소 | 대규모 리포지토리 |
| Git LFS | 대용량 파일 외부 저장 | 바이너리, 미디어 파일 |
고급 명령어 요약 표
| 명령어 | 용도 | 위험도 |
|---|---|---|
rebase | 히스토리 정리, 선형화 | 중 (공유 브랜치 주의) |
rebase -i | 커밋 squash, reword, drop | 중 |
cherry-pick | 특정 커밋 선별 적용 | 낮 |
bisect | 버그 도입 커밋 탐색 | 낮 (읽기 전용) |
stash | 임시 변경사항 보관 | 낮 |
reflog | 참조 이력 조회 및 복구 | 낮 (읽기 전용) |
worktree | 병렬 워킹 디렉토리 | 낮 |
filter-repo | 히스토리 전체 재작성 | 높 (되돌릴 수 없음) |
gc | 팩파일 생성, 저장소 최적화 | 낮 |
sparse-checkout | 부분 체크아웃 | 낮 |