이종관
글 목록으로

고급 Git: 내부 원리와 고급 명령어

Git 객체 모델, rebase/cherry-pick/bisect 등 고급 명령어와 내부 동작 원리

2025년 1월 23일·16 min read·
backend
git
version-control
devops

개요

Git을 효과적으로 활용하려면 표면적인 명령어 사용을 넘어 내부 데이터 구조와 동작 원리를 이해해야 한다. 이 노트에서는 Git의 내부 객체 모델부터 고급 명령어, 그리고 모노레포 시대의 최적화 기법까지 다룬다.

Git 내부 객체 모델

Git은 본질적으로 Content-Addressable 파일 시스템이다. 모든 데이터는 4가지 객체 타입으로 저장된다.

4가지 객체 타입

plaintext
┌──────────────────────────────────────────────┐
│                   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 참조, 태그 메시지

직접 확인하기

bash
# 커밋 객체 내용 보기
git cat-file -p HEAD
 
# 트리 객체 내용 보기
git cat-file -p HEAD^{tree}
 
# 블롭 객체 내용 보기 (특정 파일)
git cat-file -p HEAD:src/app.ts
 
# 객체 타입 확인
git cat-file -t abc1234

References (참조)

Git의 브랜치와 태그는 **커밋을 가리키는 포인터(Reference)**에 불과하다:

plaintext
.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가 브랜치가 아닌 특정 커밋을 직접 가리키는 상태
bash
# HEAD 확인
cat .git/HEAD
# ref: refs/heads/main
 
# 브랜치가 가리키는 커밋 확인
cat .git/refs/heads/main
# a1b2c3d4e5f6...

Rebase vs Merge

Merge (병합)

plaintext
main:    A ─── B ─── C ──────── M (merge commit)
                      \        /
feature:               D ─── E
  • 보존: 모든 브랜치 히스토리가 그대로 보존됨
  • 3-way merge: 공통 조상(C), 두 브랜치의 최신 커밋으로 병합
  • 머지 커밋(M)이 생성됨

Rebase (리베이스)

plaintext
main:    A ─── B ─── C
                      \
feature:               D' ─── E' (새 커밋 해시)
  • 재작성: feature 브랜치의 커밋들이 main 위에 새로 적용됨
  • 깨끗한 히스토리: 선형(Linear) 히스토리 유지
  • 커밋 해시가 변경됨 (새 커밋 생성)

Interactive Rebase (rebase -i)

bash
git rebase -i HEAD~5
plaintext
pick   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

정의

특정 커밋을 선택하여 현재 브랜치에 적용하는 명령어이다. 커밋의 변경 사항만 가져온다.

사용법

bash
# 단일 커밋 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)**을 사용하여 버그를 도입한 정확한 커밋을 찾는 명령어이다.

사용법

bash
# 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

bash
# 테스트 스크립트로 자동 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 (임시 저장)

정의

작업 중인 변경사항을 임시로 저장하고, 깨끗한 워킹 디렉토리로 돌아가는 명령어이다.

주요 사용법

bash
# 현재 변경사항 임시 저장
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)이다. 실수로 삭제하거나 리셋한 커밋을 복구할 수 있다.

사용법

bash
# 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 리포지토리에서 여러 워킹 디렉토리를 동시에 사용할 수 있게 하는 기능이다. 브랜치 전환 없이 여러 브랜치를 동시에 작업할 수 있다.

사용법

bash
# 새 워크트리 생성 (기존 브랜치)
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에 커밋하는 확장 기능이다.

설정 및 사용법

bash
# 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

동작 원리

plaintext
리포지토리 (.git)             LFS 서버 (외부)
┌─────────────────┐     ┌──────────────────┐
│ 포인터 파일       │     │ 실제 파일 내용     │
│ (SHA-256 해시,   │ ──→ │ design.psd       │
│  크기 등 메타데이터)│     │ (원본 바이너리)    │
└─────────────────┘     └──────────────────┘

git filter-repo (히스토리 재작성)

정의

Git 리포지토리의 전체 히스토리를 재작성하는 도구이다. git filter-branch의 빠르고 안전한 대체제이다.

사용 사례

bash
# 특정 파일의 전체 히스토리 삭제 (실수로 커밋된 비밀 키 등)
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)**로 압축한다.

팩파일 동작 원리

plaintext
느슨한 객체들 (초기)
.git/objects/
├── a1/b2c3d4...  (blob)
├── d4/e5f6g7...  (blob)
└── ...
 
팩파일 (압축 후)
.git/objects/pack/
├── pack-abc123.pack  (객체 데이터)
└── pack-abc123.idx   (인덱스 -- 빠른 탐색)

Delta 압축

  • 유사한 객체 간의 **차이(Delta)**만 저장
  • 큰 파일이 약간만 변경되어도 전체를 다시 저장하지 않음
  • git gc가 팩파일 생성 및 최적화 수행
bash
# 수동 팩파일 생성
git gc
 
# 리포지토리 크기 확인
git count-objects -vH
 
# 팩파일 통계
git verify-pack -v .git/objects/pack/pack-*.idx | head -20

모노레포 최적화

Shallow Clone (얕은 클론)

bash
# 최근 1개 커밋만 클론 (히스토리 불필요 시)
git clone --depth 1 https://github.com/org/monorepo.git
 
# 필요 시 히스토리 추가 fetch
git fetch --unshallow

Sparse Checkout (부분 체크아웃)

bash
# 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 list

Partial Clone (부분 클론)

bash
# 블롭 없이 클론 (필요할 때 자동 다운로드)
git clone --filter=blob:none https://github.com/org/monorepo.git
 
# 트리도 제외하여 최소한으로 클론
git clone --filter=tree:0 https://github.com/org/monorepo.git

모노레포 최적화 조합

bash
# 최적 조합: 부분 클론 + 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부분 체크아웃