git bisect: 버그를 만든 커밋을 찾는 방법
git bisect는 이진 탐색으로 이력에서 오류를 처음 도입한 커밋을 찾습니다. git bisect start로 시작하고, 문제가 있는 커밋은 git bisect bad, 정상이던 커밋은 git bisect good <해시>로 표시한 뒤, Git이 제안하는 커밋을 확인하기를 반복하면 됩니다. 마지막에는 git bisect reset으로 종료합니다.
git bisect는 이진 탐색 방식으로 오류를 도입한 커밋을 찾습니다. 버그가 있는 커밋 하나와 버그가 없던 커밋 하나를 지정하면, Git은 두 지점의 가운데로 이동해서 "여기는 정상인가요, 깨졌나요?"라고 묻습니다. 응답할 때마다 후보의 절반이 제외되므로, 커밋 1000개 중에서도 약 10번의 확인으로 범인을 찾습니다.
원인이 분명하지 않을 때 특히 유용합니다. 테스트가 실패했거나, 성능이 떨어졌거나, 화면 배치가 틀어졌는데 diff만 봐서는 이유를 모를 때입니다. 수백 개의 변경을 읽는 대신, 같은 단순한 질문을 몇 번만 던지면 됩니다.
이력에서 이진 탐색이 동작하는 방식
정상 동작하던 v2.3 릴리스와 현재 main 사이에 커밋이 200개 있다고 가정해 봅시다. Git은 가운데 커밋을 골라 확인합니다. 그 지점에서 이미 깨져 있다면 오류는 앞쪽 절반에서 생긴 것이고, 정상이라면 뒤쪽 절반에서 생긴 것입니다. 단계마다 후보가 절반으로 줄어듭니다: 200, 100, 50, 25, 13, 7, 4, 2, 1. 200번 확인 대신 8번이면 됩니다.
여기서 중요한 전제가 있습니다. "깨짐" 상태가 단조로워야 합니다. 버그가 생겼다가, 사라졌다가, 다시 생기면 bisect는 어떤 전환점을 찾기는 하지만, 여러분이 원하는 지점이 아닐 수도 있습니다.
git bisect 단계별 설명: 수동 모드
1. 정상 커밋과 문제 커밋 찾기
문제 커밋은 보통 현재 HEAD입니다. 정상 커밋은 확실히 잘 동작하던 지점, 예를 들어 지난 릴리스의 태그나 일주일 전 커밋입니다. 후보를 찾을 때는 git log --oneline, git tag, git log --since="2 weeks ago"가 도움이 됩니다.
2. 세션 시작하기
git bisect start
git bisect bad # 현재 커밋은 깨져 있음
git bisect good v2.3 # 릴리스 v2.3에서는 정상이었음
Bisecting: 99 revisions left to test after this (roughly 7 steps)
[4f5e6d7c...] 프론트엔드 의존성 업데이트
같은 내용을 한 줄로 쓸 수도 있습니다: git bisect start HEAD v2.3. 먼저 문제 커밋, 다음에 정상 커밋을 씁니다. Git은 곧바로 작업 디렉터리를 가운데 커밋으로 바꿉니다(detached HEAD 상태).
3. 확인하고 표시하기
프로젝트를 빌드하고, 테스트를 실행하거나, 버그를 직접 재현해 보세요. 그다음 결과를 알려 줍니다.
git bisect good # 여기서는 정상 동작
Bisecting: 49 revisions left to test after this (roughly 6 steps)
git bisect bad # 여기서는 깨짐
Bisecting: 24 revisions left to test after this (roughly 5 steps)
4. 결과 확인하기
후보가 하나만 남으면 Git이 그 커밋을 출력합니다.
a1b2c3d4e5f6... is the first bad commit
commit a1b2c3d4e5f6...
Author: 김민수 <minsu@example.com>
Date: Tue Sep 29 11:40:12 2026 +0300
장바구니 계산 결과 캐싱
src/cart.js | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
5. 세션 종료하기
git bisect reset
이 명령은 세션을 시작했던 브랜치로 돌려보냅니다. 이 단계를 잊지 마세요. 세션이 끝나지 않으면 detached HEAD 상태에 머물게 되고, 새로 만든 커밋을 쉽게 잃어버릴 수 있습니다. git bisect reset HEAD는 찾은 커밋에 머무르게 하고, git bisect reset <커밋>은 지정한 커밋으로 이동시킵니다.
자동 탐색: git bisect run
확인 과정을 명령 하나로 표현할 수 있다면, 전체 작업을 Git에 맡기세요. git bisect run은 각 단계에서 스크립트를 실행하고, 종료 코드로 상태를 자동 표시합니다.
| 종료 코드 | 의미 |
|---|---|
| 0 | good — 정상 커밋 |
| 1–127, 125 제외 | bad — 문제 커밋 |
| 125 | skip — 이 커밋은 확인할 수 없으니 건너뛰기 |
| 127 초과(또는 음수) | bisect 중단(예: 스크립트가 시그널로 종료됨) |
실행 예시:
git bisect start HEAD v2.3
git bisect run npm test
git bisect run pytest tests/test_cart.py -x
git bisect run make check
복잡한 확인에는 작은 스크립트를 작성하세요. "빌드가 안 됨"과 "버그가 재현됨"을 구분하는 것이 중요합니다. 그렇지 않으면 bisect가 깨진 빌드를 찾던 버그로 오인합니다.
#!/bin/sh
# check.sh
npm ci --silent || exit 125 # 의존성 설치 실패 — 건너뛰기
npm run build || exit 125 # 빌드 실패 — 건너뛰기
node scripts/check-cart-total.js # 0 — 정상, 1 — 버그
chmod +x check.sh
git bisect run ./check.sh
스크립트는 추적되는 파일 밖이나, 확인하려는 모든 커밋에 존재하는 폴더에 두세요. bisect는 작업 디렉터리를 바꾸므로, 최근에 추가한 파일은 오래된 커밋에서 사라집니다. 가장 안전한 위치는 /tmp이나 저장소 바깥의 아무 폴더입니다.
예전에는 없던 테스트
자주 있는 상황입니다. 버그를 재현하는 테스트를 작성했는데, 오래된 커밋에는 그 파일이 없습니다. 해결책은 같은 방식으로, 저장소 밖에 복사본을 두는 것입니다.
cp tests/test_regression.py /tmp/
git bisect run pytest /tmp/test_regression.py
git bisect skip: 커밋을 확인할 수 없을 때
가운데 커밋이 빌드되지 않거나 다른 이유로 실패할 때가 있습니다. 이를 good이나 bad로 표시하면 결과가 틀어집니다. 이럴 때는 건너뛰세요.
git bisect skip
git bisect skip v2.4..v2.4.3 # 범위를 건너뛰기
Git은 인접한 커밋을 선택합니다. 건너뛴 커밋이 경계에 걸리면 마지막에 하나가 아니라 "가능한 범인" 목록이 나옵니다. 그럴 때는 각 diff를 직접 확인하세요.
good과 bad 대신 직접 용어 정하기
bisect는 버그만 찾지 않습니다. 예를 들어 페이지가 빨라진 시점을 찾고 싶다면, 빠른 커밋을 "bad"라고 부르는 것은 혼란스럽습니다. 이럴 때는 다른 용어를 쓸 수 있습니다.
git bisect start --term-old=slow --term-new=fast
git bisect fast HEAD
git bisect slow v2.3
기본으로 제공되는 동의어는 old와 new입니다: git bisect new, git bisect old. 규칙은 하나입니다. "이전" 상태는 더 이른 커밋에 있어야 합니다.
세션 기록: log, replay, visualize
git bisect log— 여러분이 표시한 모든 기록을 출력합니다. 세션이 길다면 파일로 저장하세요.git bisect replay 파일— 기록을 따라 세션을 다시 실행합니다. 여섯 번째 단계에서 잘못 표시했나요? 로그를 저장하고 잘못된 줄을 지운 뒤,git bisect reset과replay를 실행하세요.git bisect visualize(또는git bisect view) — 남은 후보를 보여 줍니다.--oneline과 함께 쓰면 git log로 출력됩니다.
git bisect log > /tmp/bisect.log
# 로그 수정
git bisect reset
git bisect replay /tmp/bisect.log
bisect와 병합 커밋
병합이 많은 이력에서는 bisect가 병합된 브랜치 안의 커밋으로 들어갈 수 있습니다. 이 커밋들은 main과 동기화되기 전에 만들어졌으므로 다른 기반 위에 있습니다. 어떤 병합이 버그를 가져왔는지 먼저 알고 싶다면 다음을 사용하세요.
git bisect start --first-parent
이 옵션을 쓰면 Git은 병합 커밋의 첫 번째 부모만 따라갑니다. 즉, 주 브랜치의 이력만 탐색합니다. 문제의 병합을 찾은 뒤에는 해당 브랜치 안에서 두 번째 bisect를 실행하면 됩니다.
흔한 실수
- good과 bad를 바꿔서 표시함. 결과가 의미가 없어집니다. 처음부터 다시 하지 말고,
git bisect log와replay로 세션을 복구하세요. - 작업 디렉터리가 깨끗하지 않음. 커밋하지 않은 변경은 커밋 전환을 방해하고 확인 결과에도 영향을 줍니다. 시작 전에 커밋하거나
git stash를 하세요. - 빌드 결과물이 남아 있음. 다른 커밋에서 생긴 오래된 캐시나
node_modules는 잘못된 결과를 만듭니다. 스크립트에서 깨끗하게 빌드하세요. - 불안정한(flaky) 테스트. 테스트가 가끔 스스로 실패하면, bisect는 우연히 실패한 커밋을 범인으로 지목합니다. 여러 번 실행하거나, 탐색 전에 테스트를 고치세요.
- reset을 잊음. detached HEAD에 머문 채 작업을 시작했다면, 잃어버린 커밋은 reflog에서 찾을 수 있습니다.
예제: 처음부터 끝까지 회귀 찾기
사용자들이 할인이 적용되면 장바구니 합계가 틀린다고 알려 왔습니다. v2.3 릴리스에서는 정상이었지만 main에서는 아닙니다. 최소한의 확인 스크립트를 만들고 탐색을 실행합니다.
cat > /tmp/check.sh <<'SH'
#!/bin/sh
npm ci --silent || exit 125
node -e "const c=require('./src/cart'); process.exit(c.total([{price:100}], 0.1) === 90 ? 0 : 1)"
SH
chmod +x /tmp/check.sh
git stash # 커밋하지 않은 수정 임시 보관
git bisect start HEAD v2.3
git bisect run /tmp/check.sh
...
a1b2c3d4e5f6... is the first bad commit
git bisect log > /tmp/cart-bisect.log
git bisect reset
git stash pop
백 개의 커밋을 탐색하는 동안 여러분은 다른 일을 할 수 있습니다. 저장한 로그는 범인을 어떻게 찾았는지 동료에게 보여 줄 때 유용합니다.
찾은 커밋으로 무엇을 할까
범인을 찾았다면 git show a1b2c3d로 커밋 전체를 확인하세요. 설명이 명확한 작은 커밋은 원인을 바로 알려 줍니다. 팀에서 커밋을 작고 원자적으로 나누는 이유 중 하나가 바로 이것입니다. "fix stuff" 같은 거대한 커밋 이력에서는 bisect가 거의 쓸모가 없습니다. 이후에는 두 가지 방법이 있습니다. 새 커밋으로 오류를 고치거나, 변경 자체가 잘못되었고 이미 공개되었다면 git revert a1b2c3d로 되돌리세요.
정리
git bisect는 회귀를 찾는 일을 수백 개의 diff를 읽는 작업에서 "동작함 / 동작하지 않음"이라는 몇 번의 답변으로 바꿔 줍니다. git bisect run을 쓰면 명령 하나로 끝납니다. 필요한 것은 두 기준점, 즉 정상 커밋과 문제 커밋, 그리고 반복 가능한 확인 방법뿐입니다. 이력을 읽고, 커밋 사이를 이동하고, 그 안에서 오류를 찾는 방법은 Git 강의에서 실습으로 다룹니다.
자주 묻는 질문
git bisect는 몇 단계나 걸리나요?
대략 범위 커밋 수의 log2 값입니다. 커밋 100개면 약 7단계, 1000개면 약 10단계입니다. Git이 roughly N steps로 예상 단계를 알려 줍니다.
정상 커밋을 모를 때도 bisect를 쓸 수 있나요?
최소한 하나는 필요합니다. 확실히 오래된 커밋이나 릴리스 태그를 골라 직접 확인하세요. 범위가 넓어져서 생기는 추가 단계는 거의 비용이 들지 않습니다.
git bisect가 저장소 이력을 바꾸나요?
아니요. 커밋 사이의 작업 디렉터리만 바꾸고, 표시 정보는 refs/bisect 아래의 임시 참조에 저장합니다. 이 참조는 git bisect reset을 실행하면 삭제됩니다.
정상 커밋을 여러 개 지정할 수 있나요?
네, git bisect good에 여러 커밋을 전달할 수 있습니다. 특히 가지가 많은 이력에서 범위를 더 좁혀 줍니다.
bisect는 버그를 찾을 때만 쓰나요?
아니요. 파일이 생긴 시점, 빌드가 빨라진 시점, 동작이 바뀐 시점 등 어떤 속성이 바뀐 순간을 찾는 데도 쓸 수 있습니다. 그럴 때는 old/new나 --term-old, --term-new로 직접 정한 용어를 쓰면 편합니다.