Git 고급 명령어 마스터 — rebase, cherry-pick, bisect, reflog
git add, git commit, git push만으로 버티는 시대는 끝났다. 실무에서 커밋 히스토리가 꼬이고, 핫픽스를 특정 브랜치에만 옮겨야 하고, “어제까지는 됐는데 오늘 터졌다”를 추적해야 하는 순간이 반드시 온다. 이 글에서는 rebase, cherry-pick, bisect, reflog, stash, worktree 등 Git 고급 명령어를 실전 시나리오 중심으로 파고든다. 각 명령어가 내부적으로 무엇을 하는지, 언제 써야 하는지, 극한 상황에서 어떻게 복구하는지까지 전부 다룬다.
비유로 먼저 이해하기: Git 기본 명령어가 자동차의 액셀·브레이크·핸들이라면, 고급 명령어는 정비 도구 세트다. 평소에는 몰라도 되지만, 타이어가 펑크 나거나 엔진 경고등이 켜지면 이 도구 없이는 아무것도 할 수 없다.
1. 한 줄 요약
| 명령어 | 한 줄 설명 | 핵심 키워드 |
|---|---|---|
rebase |
커밋을 다른 베이스 위에 재배치 | 히스토리 정리 |
cherry-pick |
특정 커밋만 골라서 적용 | 핫픽스 이식 |
bisect |
이분 탐색으로 버그 커밋 찾기 | 자동 디버깅 |
reflog |
HEAD 이동 전체 기록 조회 | 실수 복구 |
stash |
작업 중 변경사항 임시 보관 | 컨텍스트 전환 |
worktree |
하나의 저장소에서 병렬 디렉토리 작업 | PR 리뷰 |
reset |
HEAD를 이동시켜 되돌리기 | 로컬 수정 |
revert |
되돌리는 새 커밋 생성 | 안전한 롤백 |
restore |
작업 디렉토리/스테이지 파일 복원 | 파일 단위 |
2. Rebase 심화 — 히스토리를 다시 쓴다
2-1. Rebase의 본질
git rebase는 커밋의 부모를 바꾸는 작업이다. 기존 커밋을 새 베이스 위에 하나씩 다시 적용(replay)하면서 새로운 커밋 해시를 생성한다. 원본 커밋은 사라지지 않지만 더 이상 어떤 브랜치도 가리키지 않게 된다.
비유: 책의 3장~5장을 1장 뒤에 끼워넣었던 것을 빼서 7장 뒤에 다시 붙이는 작업이다. 내용(diff)은 같지만 페이지 번호(커밋 해시)는 전부 바뀐다.
# feature 브랜치를 main의 최신 커밋 위로 재배치
git checkout feature
git rebase main
graph LR
A["A: main"] --> B["B"]
B --> C["C: feature"]
A --> D["D: main HEAD"]
D --> B2["B': rebase"]
B2 --> C2["C': feature HEAD"]
위 다이어그램에서 B와 C는 원본, B’과 C’은 rebase 후 새로 생성된 커밋이다. 해시가 다르지만 diff 내용은 동일하다.
2-2. Interactive Rebase — 히스토리 편집기
git rebase -i는 Git에서 가장 강력한 히스토리 편집 도구다. 커밋 순서 변경, 합치기, 메시지 수정, 삭제까지 모두 가능하다.
# 최근 5개 커밋을 대화형으로 편집
git rebase -i HEAD~5
에디터가 열리면 아래와 같은 목록이 나타난다.
pick abc1234 feat: 로그인 API 추가
pick def5678 fix: 로그인 에러 수정
pick ghi9012 fix: 오타 수정
pick jkl3456 feat: 회원가입 API 추가
pick mno7890 style: 코드 포맷팅
각 줄 앞의 명령어를 바꿔서 히스토리를 편집한다.
| 명령어 | 동작 | 언제 사용하는가 |
|---|---|---|
pick |
커밋 그대로 유지 | 기본값 |
reword |
커밋 메시지만 변경 | 오타 수정, 컨벤션 통일 |
edit |
커밋에서 멈추고 수정 가능 | 코드 변경 필요 |
squash |
이전 커밋과 합치기 (메시지 병합) | 관련 커밋 정리 |
fixup |
이전 커밋과 합치기 (메시지 버림) | 사소한 수정 흡수 |
drop |
커밋 삭제 | 불필요한 커밋 제거 |
exec |
셸 명령어 실행 | 각 커밋마다 테스트 |
2-3. Squash와 Fixup — 지저분한 커밋 정리
실무에서 가장 많이 쓰는 interactive rebase 패턴이다. “WIP”, “fix typo”, “oops” 같은 커밋을 의미 있는 단위로 합친다.
pick abc1234 feat: 로그인 API 추가
fixup def5678 fix: 로그인 에러 수정
fixup ghi9012 fix: 오타 수정
pick jkl3456 feat: 회원가입 API 추가
fixup mno7890 style: 코드 포맷팅
위 예시를 실행하면 5개의 커밋이 2개로 깔끔하게 정리된다.
비유: 편지를 세 번 고쳐 썼다면, 최종본만 봉투에 넣는 것이다. 고친 흔적(중간 커밋)은 필요 없다.
2-4. Autosquash — fixup 자동화
--fixup 플래그로 커밋하면 나중에 rebase --autosquash가 자동으로 순서를 맞춰준다.
# 작업 중 실수 발견 → fixup 커밋 생성
git commit --fixup=abc1234
# 나중에 rebase 시 자동으로 abc1234 바로 아래에 배치
git rebase -i --autosquash main
git config --global rebase.autoSquash true를 설정하면 --autosquash를 매번 쓰지 않아도 된다. 팀 전체가 이 설정을 쓰면 PR 히스토리가 놀랍도록 깔끔해진다.
2-5. Rebase vs Merge — 언제 무엇을 쓸까
두 방식 모두 브랜치를 통합하지만, 결과물의 히스토리 형태가 완전히 다르다.
| 기준 | Rebase | Merge |
|---|---|---|
| 히스토리 | 직선형 (linear) | 분기·합류형 (diamond) |
| 커밋 해시 | 변경됨 | 유지됨 |
| 충돌 해결 | 커밋마다 각각 | 한 번에 |
| 협업 안전성 | push 전에만 안전 | 항상 안전 |
| 적합한 상황 | 개인 feature 정리 | 공유 브랜치 통합 |
graph LR
M1["main"] --> M2["main"]
M2 --> MG["merge commit"]
F1["feature"] --> F2["feature"]
F2 --> MG
MG --> M3["main HEAD"]
황금 규칙: 이미 push한 커밋은 rebase하지 않는다. 다른 사람이 그 커밋 위에 작업했을 수 있기 때문이다. 개인 브랜치에서만 rebase를 쓰고, 공유 브랜치는 merge를 사용한다.
2-6. 극한 시나리오: 대규모 Rebase 충돌
feature 브랜치에 50개 커밋이 있고, main과 크게 갈라진 상태에서 rebase를 시도하면 매 커밋마다 충돌이 터질 수 있다. 이때의 전략은 다음과 같다.
전략 1: squash 먼저, rebase 나중에
# 50개 커밋을 논리 단위 3~5개로 먼저 합친다
git rebase -i HEAD~50
# squash로 정리한 뒤 main 위로 rebase
git rebase main
커밋이 적으면 충돌도 적다. 50번 충돌 해결 대신 3~5번만 하면 된다.
전략 2: rerere 활용
# rerere(reuse recorded resolution) 활성화
git config --global rerere.enabled true
rerere는 한 번 해결한 충돌 패턴을 기억해서 같은 충돌이 다시 발생하면 자동으로 해결한다. 대규모 rebase에서 같은 파일이 반복적으로 충돌할 때 시간을 크게 절약한다.
전략 3: rebase 중단과 재시작
# 충돌이 너무 심하면 중단
git rebase --abort
# 한 커밋 충돌을 해결한 뒤 계속 진행
git add .
git rebase --continue
# 현재 커밋을 건너뛰기 (내용이 이미 적용된 경우)
git rebase --skip
3. Cherry-pick — 커밋을 골라 담는다
3-1. Cherry-pick의 본질
git cherry-pick은 특정 커밋의 diff를 현재 브랜치에 새 커밋으로 복사한다. 원본 커밋은 그대로 있고, 동일한 변경사항을 가진 새 커밋이 생성된다.
비유: 다른 사람의 레시피 책에서 마음에 드는 레시피 한 장만 복사해 내 노트에 붙이는 것이다. 원본 책은 변하지 않는다.
# 특정 커밋 하나를 현재 브랜치에 적용
git cherry-pick abc1234
# 여러 커밋을 한 번에
git cherry-pick abc1234 def5678 ghi9012
graph LR
A["A"] --> B["B: hotfix"]
B --> C["C: main"]
D["D: release"] --> E["E: cherry-pick B"]
3-2. 범위 Cherry-pick
연속된 커밋 범위를 한 번에 가져올 수 있다.
# A부터 D까지 (A는 미포함, D는 포함)
git cherry-pick A..D
# A도 포함하려면
git cherry-pick A^..D
주의할 점은 A..D 표기법에서 A 자체는 포함되지 않는다는 것이다. A까지 포함하려면 A^..D 또는 A~1..D를 써야 한다.
3-3. Cherry-pick 충돌 해결
cherry-pick 도중 충돌이 발생하면 rebase와 동일한 패턴으로 해결한다.
# 충돌 발생 시 파일 수정 후
git add <충돌파일>
git cherry-pick --continue
# 중단하고 원래 상태로
git cherry-pick --abort
# 충돌 해결을 건너뛰기
git cherry-pick --skip
3-4. 실전 시나리오: 핫픽스 백포트
가장 대표적인 cherry-pick 사용 사례다. production에서 버그를 발견하고, main에서 수정한 뒤, 이전 릴리스 브랜치에도 같은 수정을 적용해야 한다.
# 1. main에서 버그 수정 커밋 확인
git log --oneline main
# abc1234 fix: null pointer 방어 코드 추가
# 2. release/1.2 브랜치로 이동
git checkout release/1.2
# 3. 해당 커밋만 적용
git cherry-pick abc1234
# 4. 커밋 메시지에 원본 정보 남기기 (권장)
git cherry-pick -x abc1234
# → 메시지에 "(cherry picked from commit abc1234)" 추가
-x 옵션은 추적성(traceability)을 확보해준다. 나중에 “이 수정이 어디서 온 건지” 추적할 때 매우 유용하다.
3-5. Cherry-pick을 쓰지 말아야 할 때
cherry-pick은 강력하지만 남용하면 히스토리가 오염된다. 동일한 diff가 서로 다른 해시로 여러 브랜치에 존재하면 나중에 merge할 때 혼란이 생긴다.
쓰지 말아야 할 때:
- feature 브랜치 전체를 옮기려는 경우 →
rebase또는merge사용 - 10개 이상의 연속 커밋을 옮기려는 경우 →
rebase --onto사용 - 같은 커밋을 3개 이상의 브랜치에 적용하려는 경우 → 공통 베이스에 merge 후 각 브랜치에서 merge
4. Bisect — 이분 탐색으로 버그 찾기
4-1. Bisect의 본질
git bisect는 이진 탐색 알고리즘을 써서 버그를 도입한 커밋을 찾는다. 1,000개 커밋 중에서도 약 10번의 테스트로 범인을 특정할 수 있다(log₂1000 ≈ 10).
비유: 1,000페이지 사전에서 단어를 찾을 때 한 페이지씩 넘기지 않는다. 중간을 펴서 앞인지 뒤인지 판단하고 절반을 버린다. bisect가 정확히 이 방식이다.
# 1. bisect 시작
git bisect start
# 2. 현재(버그 있음)를 bad로 지정
git bisect bad
# 3. 버그가 없었던 과거 커밋을 good으로 지정
git bisect good v1.0
# 4. Git이 중간 커밋으로 체크아웃 → 테스트 후 판정
git bisect good # 버그 없음
# 또는
git bisect bad # 버그 있음
# 5. 반복하면 범인 커밋이 특정됨
# abc1234 is the first bad commit
# 6. 종료
git bisect reset
graph LR
G["good: v1.0"] --> M1["테스트 1"]
M1 -->|"good"| M2["테스트 2"]
M2 -->|"bad"| M3["테스트 3"]
M3 -->|"발견!"| BAD["범인 커밋"]
4-2. Bisect Run — 자동화 스크립트
수동으로 매번 good/bad를 판정하는 건 번거롭다. 테스트 스크립트를 연결하면 완전 자동화할 수 있다.
# 테스트 스크립트: 종료 코드 0이면 good, 1~127이면 bad, 125면 skip
git bisect start HEAD v1.0
git bisect run ./test-bug.sh
test-bug.sh 예시:
#!/bin/bash
# 빌드
./gradlew build -q 2>/dev/null
if [ $? -ne 0 ]; then
exit 125 # 빌드 실패 → skip (판정 불가)
fi
# 특정 테스트 실행
./gradlew test --tests "com.example.LoginTest" -q 2>/dev/null
if [ $? -eq 0 ]; then
exit 0 # 테스트 통과 → good
else
exit 1 # 테스트 실패 → bad
fi
종료 코드 규칙이 중요하다. 125는 “이 커밋은 판정할 수 없으니 건너뛰라”는 의미다. 빌드 자체가 실패하는 커밋에 사용한다.
4-3. 실전 시나리오: “어제까지 됐는데 오늘 안 돼요”
QA팀에서 “로그인 후 프로필 페이지가 500 에러를 반환한다”고 보고했다. 지난주 금요일까지는 정상이었다. 그 사이 커밋이 87개다.
git bisect start
git bisect bad HEAD
git bisect good HEAD~87
git bisect run npm test -- --grep "profile page"
# ... 약 7번 만에 범인 특정
# commit def5678 is the first bad commit
# Author: 김개발
# Date: 월요일
# Message: refactor: 유저 세션 구조 변경
git bisect reset
7번의 자동 테스트로 87개 커밋 중 범인을 찾았다. 수동 디버깅으로 반나절 걸릴 작업이 2분에 끝난다.
5. Reflog — Git의 블랙박스 레코더
5-1. Reflog의 본질
git reflog는 HEAD와 브랜치 포인터의 모든 이동 기록을 보여준다. git log가 커밋 히스토리라면, reflog는 내가 한 모든 Git 작업의 타임라인이다.
비유: 비행기의 블랙박스와 같다. 사고(실수)가 발생하면 블랙박스를 열어 사고 직전 상태로 되돌릴 수 있다. 다만 블랙박스는 영원하지 않다 — 기본 90일 후 만료된다.
# HEAD의 이동 기록 확인
git reflog
# abc1234 HEAD@{0}: commit: feat: 새 기능
# def5678 HEAD@{1}: rebase: squash
# ghi9012 HEAD@{2}: checkout: moving from main to feature
# jkl3456 HEAD@{3}: reset: moving to HEAD~3
# ...
# 특정 브랜치의 reflog
git reflog show feature
5-2. 삭제된 커밋 복구
git reset --hard HEAD~3으로 커밋 3개를 날렸다고 가정하자. git log에는 보이지 않지만 reflog에는 남아 있다.
# 실수: 커밋 3개 삭제
git reset --hard HEAD~3
# reflog에서 삭제 전 커밋 해시 확인
git reflog
# abc1234 HEAD@{0}: reset: moving to HEAD~3
# def5678 HEAD@{1}: commit: 중요한 작업 3
# ghi9012 HEAD@{2}: commit: 중요한 작업 2
# jkl3456 HEAD@{3}: commit: 중요한 작업 1
# 삭제 전 상태로 복구
git reset --hard def5678
커밋 3개가 완벽하게 복구된다. reflog가 없었다면 이 커밋들은 영영 사라진다(정확히는 GC 전까지 dangling object로 존재하지만, 찾기가 극히 어렵다).
5-3. 삭제된 브랜치 복구
실수로 브랜치를 삭제한 경우에도 reflog로 복구할 수 있다.
# 실수: feature 브랜치 삭제
git branch -D feature
# reflog에서 feature의 마지막 커밋 찾기
git reflog | grep "feature"
# abc1234 HEAD@{5}: checkout: moving from feature to main
# 해당 커밋에서 브랜치 재생성
git branch feature abc1234
5-4. 극한 시나리오: force push 사고 복구
팀원이 실수로 git push --force를 해서 원격 main 브랜치의 히스토리가 날아갔다.
# 1. 사고 발생 직후, force push를 하지 않은 팀원의 로컬에서
git reflog show main
# → force push 이전 커밋 해시 확인: abc1234
# 2. 해당 팀원이 올바른 상태를 push
git push origin abc1234:refs/heads/main --force
# 3. 또는 GitHub에서 branch protection rule이 있었다면
# GitHub Support에 연락하여 복구 요청
예방책:
main과release브랜치에 branch protection rule 설정- force push 금지, PR 필수, status check 통과 필수
git config --global push.default current로 실수 범위 최소화
5-5. Reflog 만료 설정
# 기본: 도달 가능한 항목 90일, 도달 불가능한 항목 30일
git config --global gc.reflogExpire 90.days
git config --global gc.reflogExpireUnreachable 30.days
# 중요 프로젝트에서 더 오래 보관
git config gc.reflogExpire 180.days
6. Stash — 작업을 잠시 보관한다
6-1. Stash의 본질
git stash는 현재 작업 중인 변경사항을 스택(stack)에 임시 저장하고 작업 디렉토리를 깨끗한 상태로 되돌린다. 급하게 다른 브랜치에서 작업해야 할 때 커밋하지 않고 컨텍스트를 전환할 수 있다.
비유: 책상에서 보고서를 쓰다가 급한 전화가 왔다. 쓰던 서류를 서랍(stash)에 넣고 전화를 받은 뒤, 서랍에서 꺼내 이어서 쓰는 것이다.
# 기본 stash
git stash
# 메시지와 함께 저장 (권장)
git stash push -m "로그인 API 작업 중"
# stash 목록 확인
git stash list
# stash@{0}: On feature: 로그인 API 작업 중
# stash@{1}: WIP on main: abc1234 이전 작업
# 가장 최근 stash 복원
git stash pop
# 특정 stash 복원 (삭제하지 않음)
git stash apply stash@{1}
6-2. 고급 Stash 옵션
–keep-index: 스테이지된 파일은 유지
# staged 파일은 그대로 두고 unstaged만 stash
git stash push --keep-index -m "unstaged만 보관"
이 옵션은 “스테이징한 파일만 먼저 테스트하고 싶을 때” 유용하다. staged 변경사항으로 빌드/테스트를 돌리고, 나머지는 stash에 보관한다.
–include-untracked: 추적되지 않는 파일도 포함
# 새로 만든 파일까지 stash에 포함
git stash push --include-untracked -m "새 파일 포함"
# 축약형
git stash push -u -m "새 파일 포함"
기본 stash는 추적 중인 파일의 변경사항만 저장한다. 새로 만든 파일(untracked)은 빠진다. -u 옵션으로 함께 보관해야 작업 디렉토리가 완전히 깨끗해진다.
–patch: 변경사항 일부만 stash
# 변경사항을 hunk 단위로 선택하여 stash
git stash push --patch -m "일부만 보관"
6-3. Stash를 브랜치로 변환
stash를 복원할 때 충돌이 발생하면, stash 내용을 별도 브랜치로 만들 수 있다.
# stash 내용으로 새 브랜치 생성
git stash branch new-feature-from-stash stash@{0}
이 명령은 stash가 생성된 시점의 커밋에서 새 브랜치를 만들고, stash 내용을 적용한 뒤, stash를 삭제한다. 충돌 가능성이 0이다.
6-4. 실전 시나리오: 긴급 핫픽스
feature 브랜치에서 새 기능을 개발하다가 production 장애 연락이 왔다.
# 1. 현재 작업 저장
git stash push -u -m "feature/login: OAuth 연동 작업 중"
# 2. hotfix 브랜치로 이동하여 수정
git checkout -b hotfix/null-check main
# ... 수정 작업 ...
git commit -m "fix: null pointer 방어"
git push origin hotfix/null-check
# 3. 원래 작업으로 복귀
git checkout feature/login
git stash pop
# → OAuth 작업이 그대로 복원됨
7. Worktree — 병렬 작업 공간
7-1. Worktree의 본질
git worktree는 하나의 .git 저장소에서 여러 개의 작업 디렉토리를 만든다. 각 디렉토리는 서로 다른 브랜치를 체크아웃할 수 있다. clone을 여러 번 하는 것보다 디스크 공간을 절약하고, .git 객체를 공유하므로 fetch도 한 번이면 된다.
비유: 요리사가 하나의 냉장고(저장소)를 공유하면서 별도의 조리대(worktree)를 두는 것이다. 각 조리대에서 다른 요리(브랜치)를 동시에 만들 수 있고, 재료(Git 객체)는 공유한다.
# main 브랜치용 워크트리 추가
git worktree add ../project-main main
# feature 브랜치용 워크트리 추가
git worktree add ../project-feature feature/login
# 워크트리 목록 확인
git worktree list
# /home/user/project abc1234 [develop]
# /home/user/project-main def5678 [main]
# /home/user/project-feature ghi9012 [feature/login]
# 워크트리 제거
git worktree remove ../project-main
graph LR
GIT[".git 저장소"] --> W1["worktree: main"]
GIT --> W2["worktree: develop"]
GIT --> W3["worktree: feature"]
7-2. 실전 시나리오: PR 리뷰
코드 리뷰를 할 때 현재 작업을 stash하고 브랜치를 전환하는 건 번거롭다. worktree를 쓰면 현재 작업을 중단하지 않고 리뷰할 수 있다.
# PR 리뷰용 워크트리 생성
git fetch origin
git worktree add ../review-pr-42 origin/feature/payment
# 별도 터미널에서 리뷰
cd ../review-pr-42
# ... 코드 확인, 빌드, 테스트 ...
# 리뷰 끝나면 제거
git worktree remove ../review-pr-42
7-3. 실전 시나리오: 장기 브랜치 병렬 관리
release 브랜치와 develop 브랜치를 동시에 관리해야 하는 경우 각각 worktree를 만들어두면 브랜치 전환 없이 작업할 수 있다.
git worktree add ../release release/2.0
git worktree add ../develop develop
# 터미널 1: cd ../release → 핫픽스 작업
# 터미널 2: cd ../develop → 신규 기능 작업
8. Reset vs Revert vs Restore — 되돌리기 3총사 비교
이 세 명령어는 모두 “되돌리기”를 하지만 동작 방식이 완전히 다르다. 혼동하면 히스토리가 꼬인다.
8-1. 핵심 차이
| 구분 | reset | revert | restore |
|---|---|---|---|
| 대상 | HEAD 포인터 이동 | 되돌리는 새 커밋 생성 | 파일 내용 복원 |
| 히스토리 | 변경 (커밋 삭제/이동) | 추가 (새 커밋 추가) | 변경 없음 |
| 안전성 | 위험 (push 후 사용 금지) | 안전 (공유 브랜치 OK) | 안전 (파일 단위) |
| 범위 | 커밋 단위 | 커밋 단위 | 파일 단위 |
graph LR
A["A"] --> B["B"] --> C["C: HEAD"]
C -->|"reset"| A
C -->|"revert"| D["D: B 취소 커밋"]
C -->|"restore"| E["파일만 복원"]
8-2. Reset의 3가지 모드
# --soft: HEAD만 이동, 스테이지·작업디렉토리 유지
git reset --soft HEAD~1
# → 커밋을 취소하고 변경사항은 staged 상태로 남김
# --mixed (기본): HEAD 이동 + 스테이지 해제
git reset HEAD~1
# → 커밋을 취소하고 변경사항은 unstaged 상태로 남김
# --hard: HEAD 이동 + 스테이지 해제 + 작업디렉토리 초기화
git reset --hard HEAD~1
# → 커밋과 변경사항 모두 삭제 (reflog으로 복구 가능)
| 모드 | HEAD | Stage | Working Dir |
|---|---|---|---|
--soft |
이동 | 유지 | 유지 |
--mixed |
이동 | 초기화 | 유지 |
--hard |
이동 | 초기화 | 초기화 |
8-3. Revert — 안전한 롤백
이미 push한 커밋을 되돌려야 할 때는 반드시 revert를 사용한다. 원본 커밋은 히스토리에 남고, 그 변경사항을 취소하는 새 커밋이 추가된다.
# 특정 커밋 되돌리기
git revert abc1234
# 연속 커밋 범위 되돌리기 (각각 revert 커밋 생성)
git revert abc1234..def5678
# 하나의 revert 커밋으로 합치기
git revert --no-commit abc1234..def5678
git commit -m "revert: 잘못된 배포 롤백"
8-4. Restore — 파일 단위 복원
Git 2.23에서 도입된 명령어로, checkout의 파일 복원 기능을 분리한 것이다.
# 작업 디렉토리의 파일을 마지막 커밋 상태로 복원
git restore src/main.java
# 스테이지에서 내리기 (unstage)
git restore --staged src/main.java
# 특정 커밋의 파일 상태로 복원
git restore --source=abc1234 src/main.java
8-5. 실전 판단 플로우
“이 커밋을 되돌려야 한다”는 상황에서의 판단 기준:
- 아직 push 안 했다 →
git reset으로 깔끔하게 제거 - 이미 push 했다 →
git revert로 새 커밋 생성 - 특정 파일만 되돌리고 싶다 →
git restore --source=<commit> <file> - 스테이지만 취소하고 싶다 →
git restore --staged <file>
9. Blame & Log 고급 — 코드 고고학
9-1. git blame — 줄마다 범인 찾기
# 파일의 각 줄이 누가, 언제, 어떤 커밋에서 작성했는지
git blame src/UserService.java
# 특정 줄 범위만
git blame -L 10,20 src/UserService.java
# 공백 변경 무시 (포맷팅 커밋 건너뛰기)
git blame -w src/UserService.java
# 코드 이동/복사도 추적 (-C -C -C)
git blame -C -C -C src/UserService.java
-C 옵션의 개수에 따라 추적 범위가 달라진다.
| 옵션 | 추적 범위 |
|---|---|
-C |
같은 커밋 내 파일 간 이동 |
-C -C |
파일 생성 커밋에서의 복사 |
-C -C -C |
모든 커밋에서의 복사 |
9-2. git log -S — “Pickaxe” 검색
특정 문자열이 추가되거나 삭제된 커밋을 찾는다. “이 함수는 언제 추가됐지?”를 찾을 때 사용한다.
# "calculatePrice" 문자열이 추가/삭제된 커밋 검색
git log -S "calculatePrice" --oneline
# 정규식 사용
git log -G "calculate.*Price" --oneline
# 파일 이동 추적
git log --follow -- src/utils/price.js
-S와 -G의 차이를 반드시 이해해야 한다.
| 옵션 | 동작 | 용도 |
|---|---|---|
-S |
diff에서 해당 문자열의 출현 횟수가 변한 커밋 | 함수 추가/삭제 시점 |
-G |
diff에서 해당 패턴이 포함된 행이 변경된 커밋 | 함수 내용 수정 추적 |
9-3. 실전 시나리오: “이 코드 왜 이렇게 되어 있지?”
레거시 코드에서 이상한 로직을 발견했을 때의 조사 과정이다.
# 1. 누가 이 줄을 작성했는지 확인
git blame -L 42,42 src/OrderService.java
# abc1234 (김개발 2024-03-15) if (order.getStatus() == null) return;
# 2. 해당 커밋의 전체 내용 확인
git show abc1234
# 3. 이 줄이 변경된 전체 히스토리
git log -L 42,42:src/OrderService.java
# 4. 관련 이슈나 PR 링크 찾기
git log --grep="null check" --oneline
9-4. –follow: 파일 이름 변경 추적
파일 이름을 바꾸면 기본 git log는 이름 변경 이전 히스토리를 보여주지 않는다.
# 이름 변경 이전 히스토리까지 추적
git log --follow -- src/service/UserService.java
# 이전 이름: src/UserService.java → src/service/UserService.java
# --follow 없이는 이름 변경 이후 커밋만 보인다
10. 실무 실수 TOP 5와 복구 방법
10-1. 실수 1: 잘못된 브랜치에 커밋
develop에 해야 할 커밋을 main에 해버렸다.
# 1. main에서 커밋 해시 확인
git log --oneline -1
# abc1234 feat: 새 기능
# 2. develop 브랜치에 cherry-pick
git checkout develop
git cherry-pick abc1234
# 3. main에서 해당 커밋 제거
git checkout main
git reset --hard HEAD~1
10-2. 실수 2: git reset –hard 후 코드 소실
# reflog로 즉시 복구
git reflog
# abc1234 HEAD@{1}: commit: 소중한 작업
git reset --hard abc1234
10-3. 실수 3: 커밋 메시지 오타
# 가장 최근 커밋 메시지 수정
git commit --amend -m "올바른 메시지"
# 이전 커밋 메시지 수정 (3번째 전)
git rebase -i HEAD~3
# → 해당 커밋을 reword로 변경
10-4. 실수 4: .env 파일을 커밋에 포함
민감한 파일이 커밋에 포함된 경우, 히스토리에서 완전히 제거해야 한다.
# 1. .gitignore에 추가
echo ".env" >> .gitignore
# 2. 추적에서 제거 (파일은 삭제하지 않음)
git rm --cached .env
git commit -m "chore: .env 추적 제거"
# 3. 히스토리에서도 완전 제거 (push 전이라면)
git filter-branch --force --index-filter \
"git rm --cached --ignore-unmatch .env" \
--prune-empty -- --all
# 또는 BFG Repo-Cleaner 사용 (더 빠르고 안전)
bfg --delete-files .env
git reflog expire --expire=now --all
git gc --prune=now --aggressive
주의: 이미 push한 경우, 시크릿이 노출된 것으로 간주하고 즉시 키를 교체해야 한다. 히스토리에서 제거해도 누군가 이미 clone했을 수 있다.
10-5. 실수 5: merge 커밋을 revert하고 다시 merge가 안 됨
merge 커밋을 revert하면, Git은 해당 브랜치의 변경사항이 “이미 적용됨”으로 판단하여 다시 merge해도 변경사항이 반영되지 않는다.
# merge 커밋 revert (parent 1번 기준으로 되돌림)
git revert -m 1 <merge-commit-hash>
# 나중에 같은 브랜치를 다시 merge하면?
# → 변경사항이 없다고 나온다!
# 해결: revert를 revert한다 (revert의 revert)
git revert <revert-commit-hash>
# 그 다음 merge
git merge feature
비유: 택배를 받았다가 반품(revert)했다. 같은 물건을 다시 주문(merge)하면 택배사는 “이미 배송 완료”라고 한다. 반품 취소(revert의 revert)를 먼저 해야 재배송이 가능하다.
11. 극한 시나리오: 커밋 히스토리 오염 정리
11-1. 상황: 수백 개의 “WIP” 커밋이 main에 들어감
누군가 squash 없이 feature 브랜치를 main에 merge했고, “WIP”, “fix typo”, “oops” 같은 커밋 수백 개가 main 히스토리를 오염시켰다.
# 1. merge 커밋 직전으로 돌아가기
git log --oneline --merges -5
# abc1234 Merge branch 'feature/messy'
# def5678 Merge branch 'feature/clean'
# 2. merge를 취소하고 squash merge로 다시
git revert -m 1 abc1234
git merge --squash feature/messy
git commit -m "feat: 새 기능 (히스토리 정리)"
11-2. 상황: 대용량 바이너리가 히스토리에 존재
실수로 500MB 동영상 파일을 커밋했다가 삭제해도 .git 디렉토리에는 남아있다.
# 어떤 파일이 용량을 차지하는지 확인
git rev-list --objects --all |
git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' |
sort -rnk3 | head -10
# BFG로 100MB 이상 파일 제거
bfg --strip-blobs-bigger-than 100M
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --force
11-3. 상황: rebase 도중 포기 후 꼬인 상태
rebase 도중에 --abort를 안 하고 다른 작업을 해버린 경우다.
# 현재 rebase 상태 확인
git status
# interactive rebase in progress; onto abc1234
# rebase가 진행 중이면 중단
git rebase --abort
# 이미 꼬였다면 reflog에서 rebase 시작 전 상태 복구
git reflog
# abc1234 HEAD@{5}: rebase: start
# def5678 HEAD@{6}: checkout: moving from feature to feature
git reset --hard def5678
12. 명령어 조합 실전 워크플로
12-1. PR 정리 워크플로
# 1. feature 브랜치에서 작업 완료
git checkout feature/payment
# 2. main 최신 상태 가져오기
git fetch origin main
# 3. 커밋 정리 (squash + reword)
git rebase -i origin/main
# 4. main 위로 rebase
git rebase origin/main
# 5. force push (개인 브랜치이므로 안전)
git push --force-with-lease origin feature/payment
--force-with-lease는 --force보다 안전하다. 원격에 예상하지 못한 커밋이 있으면 push를 거부한다. 다른 사람이 같은 브랜치에 push한 경우를 방지한다.
graph LR
A["main fetch"] --> B["rebase -i 정리"]
B --> C["rebase main"]
C --> D["force-with-lease"]
D --> E["PR 생성"]
12-2. 릴리스 핫픽스 워크플로
# 1. main에서 핫픽스 커밋
git checkout main
git commit -m "fix: critical bug"
# → 커밋 해시: abc1234
# 2. release 브랜치에 cherry-pick
git checkout release/2.0
git cherry-pick -x abc1234
# 3. develop에도 반영
git checkout develop
git cherry-pick -x abc1234
12-3. 대규모 리팩토링 워크플로
# 1. worktree로 리팩토링 전용 공간 생성
git worktree add ../refactor feature/refactor
# 2. 리팩토링 작업 (별도 터미널)
cd ../refactor
# ... 작업 ...
# 3. 본래 워크트리에서는 일상 업무 계속
# (브랜치 전환 불필요)
# 4. 리팩토링 완료 후 worktree 제거
git worktree remove ../refactor
13. 자주 쓰는 Git Alias 설정
고급 명령어는 타이핑이 길다. alias를 설정하면 효율이 크게 올라간다.
git config --global alias.lg "log --oneline --graph --all --decorate"
git config --global alias.st "status -sb"
git config --global alias.unstage "restore --staged"
git config --global alias.last "log -1 HEAD --stat"
git config --global alias.amend "commit --amend --no-edit"
git config --global alias.wip "stash push -u -m"
git config --global alias.unwip "stash pop"
git config --global alias.cp "cherry-pick"
git config --global alias.ri "rebase -i"
git config --global alias.rl "reflog --format='%C(auto)%h %gd %gs %C(blue)(%cr)'"
# 사용 예시
git lg # 그래프 히스토리
git st # 간결한 상태
git unstage . # 전체 unstage
git wip "작업중" # 이름 있는 stash
git ri HEAD~5 # interactive rebase
14. 고급 명령어 내부 동작 이해
14-1. Git의 객체 모델
고급 명령어를 정확히 이해하려면 Git의 내부 구조를 알아야 한다. Git은 4가지 객체로 모든 것을 관리한다.
| 객체 | 역할 | 불변성 |
|---|---|---|
| blob | 파일 내용 | 불변 (SHA-1 해시) |
| tree | 디렉토리 구조 | 불변 |
| commit | 스냅샷 + 메타데이터 + 부모 포인터 | 불변 |
| tag | 특정 커밋에 대한 이름 | 불변 |
rebase가 “커밋을 수정한다”고 했지만, 실제로는 기존 커밋은 그대로 두고 새 커밋을 생성한 뒤 브랜치 포인터를 옮기는 것이다. 기존 커밋은 어떤 ref도 가리키지 않는 dangling 상태가 되어 나중에 GC가 정리한다.
graph LR
REF["브랜치 포인터"] --> C2["새 커밋 C'"]
C2 --> B2["새 커밋 B'"]
B2 --> A["base 커밋 A"]
C1["구 커밋 C"] -.->|"dangling"| B1["구 커밋 B"]
14-2. reflog이 동작하는 이유
reflog은 .git/logs/ 디렉토리에 텍스트 파일로 저장된다. HEAD가 이동할 때마다 한 줄이 추가된다.
# reflog 파일 직접 확인
cat .git/logs/HEAD
# 이전해시 이후해시 사용자 <이메일> 타임스탬프 동작설명
이것이 reflog가 “최후의 보루”인 이유다. 브랜치 포인터가 바뀌어도, reset으로 커밋이 사라져도, rebase로 히스토리가 재작성되어도 reflog에는 이전 상태의 해시가 텍스트로 기록되어 있다.
15. 명령어별 위험도 분류
실무에서 명령어를 쓸 때 위험도를 인식하고 있어야 한다.
| 위험도 | 명령어 | 이유 |
|---|---|---|
| 안전 | log, reflog, blame, bisect, stash, worktree |
히스토리를 변경하지 않음 |
| 로컬 위험 | reset --hard, rebase, commit --amend |
로컬 히스토리 변경, push 전 복구 가능 |
| 원격 위험 | push --force, filter-branch |
팀 전체에 영향, 복구 어려움 |
안전 규칙:
push --force대신 항상push --force-with-lease사용- rebase는 개인 브랜치에서만
reset --hard전에 현재 HEAD 해시를 메모- 공유 브랜치 되돌리기는 반드시
revert
댓글