어젯밤 CI에서 통과하던 payment.spec.js가 아침 빌드에서 실패했습니다. 마지막 정상 릴리스 태그 이후로 머지된 커밋은 200개입니다. git log를 위로 스크롤하며 diff를 눈으로 훑으면 선형 탐색에 해당하므로, 매 커밋 빌드와 테스트를 돌리는 것은 상당한 시간을 소모합니다. 이 상황을 직접 겪은 사례가 아니라, 그런 회귀가 발생한 가상의 시나리오로 가정해 보겠습니다.
이때 git bisect는 “하나씩 확인”하는 대신 “매번 절반씩 배제”하는 방식을 취합니다. 정상으로 확인한 커밋(good)과 실패가 발견된 커밋(bad)을 경계로 삼으면, Git은 후보 집합의 중간 커밋을 체크아웃합니다. 사용자는 해당 커밋을 테스트해 good 또는 bad로 답해야 합니다. 매 판정마다 후보 수가 절반으로 줄어들기 때문에, 커밋이 1,000개여도 최대 약 10회(log₂1000 ≈ 10)면 범위를 좁힐 수 있습니다 1. 200개라면 8회 안팎으로 수렴합니다.
다만 이 계산은 모든 판정이 실제 커밋의 동작을 반영할 때만 성립합니다. 빌드가 깨져 실행 자체가 불가능한 커밋을 bad로 잘못 판단하거나, 자동화 스크립트가 실행조차 안 되어 exit code 127을 반환하면 상황이 달라집니다. Git은 잘못된 절반을 선택하거나 모든 커밋을 bad로 표시하여 결과를 무의미하게 만듭니다 2.
탐색을 시작하기 전, 재현 조건과 경계 준비
git bisect가 제대로 작동하려면 두 지점의 상태가 명확해야 합니다. 현재 HEAD가 bad라는 것은 회귀가 재현된다는 뜻이고, 과거의 어느 태그가 good라는 것은 그 시점에는 회귀가 없다는 뜻입니다. 이 전제가 흔들리면 전체 탐색 과정이 어긋납니다.
Nodelog Engineer는 good으로 잡은 지점이 사실은 이미 고장난 상태였다면 결과가 왜곡될 수 있다고 지적합니다. 따라서 “최근에 정상으로 보인 커밋”보다는 “버그가 생기기 전임이 분명한 태그”를 넉넉히 앞당겨 good으로 잡는 것이 좋습니다 1.
판정을 사람이 아니라 스크립트로 내릴 때는 스크립트가 실행되는 위치도 결과에 영향을 미칩니다. tms-outsource는 판정 스크립트를 저장소 밖에 두는 편이 안전하다고 설명합니다. 그 이유는 이전 커밋에는 나중에 추가된 테스트 파일이 존재하지 않아, 테스트 파일 부재라는 잘못된 이유로 실패가 발생할 수 있기 때문입니다 2. 200개 커밋 뒤에서 추가된 payment.spec.js를 탐색 범위에 포함된 과거 커밋에서 실행하려 하면, 그 커밋의 코드 문제인지 테스트 파일 유무 문제인지 구별할 수 없습니다.
따라서 회귀를 검증하는 명령은 저장소 외부 경로에 두거나, 의존성 설치(npm ci 등)를 포함해 격리된 상태로 준비해야 합니다 1. 이렇게 해야 이전 커밋의 환경 잔재로 인한 오판을 피할 수 있습니다. 탐색 전에 작업 공간을 깔끔히 정리하는 것도 좋습니다. 미커밋된 변경 사항이 체크아웃을 방해할 수 있으므로 git stash 등을 사용해 변경 사항을 분리해 두는 것이 권장됩니다 2.
good과 bad 경계를 설정해 탐색 범위를 좁히기

세션은 git bisect start로 시작합니다. 실패가 발견된 현재 커밋을 git bisect bad로, 정상이 확인된 지점을 git bisect good으로 등록합니다. GitLab 공식 문서는 이 순서를 “bad 커밋과 good 커밋을 식별해 경계값을 지정한다”로 요약합니다 3.
git bisect start
git bisect bad # 현재 HEAD = 회귀 재현
git bisect good v1.4.0 # 태그·커밋 해시·상대참조 모두 가능시작점에 두 리비전을 한꺼번에 넘기는 축약형도 사용할 수 있습니다. git bisect start HEAD v1.4.0처럼 bad를 먼저, good을 뒤에 적으면 두 경계가 한 번에 지정됩니다 2.
탐색 공간 자체를 줄이는 수단도 있습니다. 특정 경로에 변경이 있는 커밋만 대상으로 삼는 pathspec을 추가할 수 있습니다. Git 문서 예시로는 git bisect start -- arch/i386 include/asm-i386처럼 특정 디렉토리나 파일 변경만 후보로 제한하는 방법이 있습니다 2. 가상의 결제 로직 예에서는 관련 없는 문서 변경이나 CI 설정 변경을 후보에서 제외하는 데 활용할 수 있습니다.
또한, known-good 커밋을 여러 개 지정하면 탐색 공간이 더 좁혀집니다. bad 커밋 뒤에 여러 개의 정상 커밋을 나열하면 범위를 추가로 축소할 수 있습니다 2. 탐색 범위가 넓을수록 log₂ 계산의 이득이 커지므로, 이러한 전처리는 “실패 지점”보다 “경계의 넓이”를 줄이는 데 효과적입니다.
중간 커밋 검사, skip을 통한 우회와 시각 확인

두 경계가 정해지면 Git은 후보 집합의 중간 지점 커밋으로 자동으로 체크아웃합니다. 이때 Git은 남은 후보 수와 예상 단계를 함께 출력합니다.
Bisecting: 63 revisions left to test after this (roughly 6 steps)
이 출력물은 사용자가 판정을 내려야 할 횟수를 미리 가늠할 수 있게 해줍니다. 공식 문서 예시에서도 675 revisions left, roughly 10 steps처럼 후보가 반으로 줄어드는 흐름이 확인됩니다 12.
사용자는 체크아웃된 커밋을 빌드하고 회귀 재현 여부를 확인한 뒤, 결과만 입력합니다.
# 회귀가 재현되면
git bisect bad
# 정상이면
git bisect good중간 커밋을 bad로 답하면 “범인은 그 이전(또는 여기)”이라는 뜻이므로 Git은 이후 구간을 배제합니다. good로 답하면 “범인은 그 이후”라는 뜻이므로 이전 구간을 배제합니다. 이 규칙이 log₂ 단계로 후보를 소멸시킵니다 2. 최종적으로는 b9f0e1a2c3... is the first bad commit처럼 단일 해시와 해당 커밋이 수정한 파일 목록이 표시됩니다 1.
빌드 자체가 실패해 good/bad를 판단할 수 없는 커밋도 발생할 수 있습니다. 이때는 git bisect skip으로 현재 커밋을 건너뛸 수 있습니다. Git은 근처의 다른 커밋으로 우회해 탐색을 이어갑니다 1. skip은 범위 지정도 가능합니다. git bisect skip v2.5..v2.6처럼 구간을 통째로 표시할 수 있습니다 1.
단, skip에는 정밀도 희생이라는 대가가 따릅니다. Git 공식 문서 매뉴얼을 인용한 tms-outsource의 설명에 따르면, 범인 커밋과 인접한 커밋을 skip하면 Git은 “둘 중 무엇이 먼저인지”를 정확히 지목할 수 없습니다 2. 탐색 중단을 피하기 위한 수단이지만 정밀도가 떨어질 수 있습니다. skip된 후보가 인접해 구별이 불가능할 경우, Git은 후보 목록을 모두 찍고 멈춥니다. 이때는 git bisect visualize를 사용해 gitk에서 남은 의심 후보를 시각적으로 확인하고, 인접한 정상 커밋 하나를 골라 수동으로 판정하는 방식으로 복구할 수 있습니다 1.
자동화: git bisect run과 exit code가 결과를 좌우한다

사람이 매 단계 good/bad를 입력하는 대신, 스크립트의 종료 코드로 판정을 자동화할 수 있습니다. git bisect run <스크립트>가 각 체크아웃된 커밋에서 명령을 실행하고, exit code를 판정으로 해석합니다.
Christian Couder(LWN.net, 2009년 2월 3일 기고)는 종료 코드가 0이면 good, 그 외 값(125 및 128 이상 제외)이면 bad로 해석된다고 명시했습니다. 빌드 실패로 회귀 여부 판단이 불가능한 커밋은 125로 skip을 요청하는 패턴을 제시하기도 했습니다 4.
git bisect start
git bisect bad
git bisect good v1.4.0
git bisect run npm test -- payment.spec.js| exit code | 판정 | 상황 |
|---|---|---|
| 0 | good | 테스트 통과 |
| 1~124, 126, 127 | bad | 테스트 실패 |
| 125 | skip | 빌드 실패 등 검증 불가 |
| 128~255 | abort | 탐색 중단(시그널, 의도적 종료 등) |
여기서 주의해야 할 함정 두 가지를 짚어야 합니다.
첫째, exit code 127입니다. POSIX 셸은 명령을 찾지 못했을 때 127을 반환합니다. 스크립트 경로에 오타가 있어 명령 자체가 실행되지 않으면 127이 나옵니다. Git은 이를 “bad”로 해석해 모든 커밋을 bad로 표시하고, 탐색은 무의미한 결과로 끝납니다 2. “테스트가 실패했다”와 “스크립트가 존재하지 않아 실행조차 못 했다”가 같은 bad로 묶이기 때문입니다. 따라서 자동화 전에 스크립트가 실행 가능한 상태인지 반드시 확인해야 합니다 2.
둘째, 빌드 실패와 회귀 실패의 구분입니다. 테스트 실패가 아닌 컴파일 오류로 exit code 1이나 2가 반환되면 그 커밋은 bad로 기록됩니다. 하지만 실패 원인이 회귀가 아니라 빌드 환경일 수 있습니다. Couder는 빌드 실패 시 skip을 명확히 하기 위해 make || exit 125 패턴을 제시합니다 4.
#!/usr/bin/env bash
# 저장소 밖에 두는 편이 안전합니다 [S13]
make build || exit 125 # 빌드 실패 = 검증 불가 → skip
if ./run-app --check-payment; then
exit 0 # good
else
exit 1 # bad (회귀 재현)
fi이 한 줄의 조건절이 “빌드 실패”와 “회귀”를 분리해 주는 핵심 장치입니다. 저장소 외부 경로에 이 스크립트를 두면 과거 커밋에는 테스트 파일이 없어 발생하는 오판도 줄일 수 있습니다 2.
자동화에는 전제 조건도 있습니다. Couder의 기고에서 Andreas Ericsson(Git 개발자)은 “git bisect을 쓰려면 모든 커밋이 작고 bisect 가능한 상태로 유지되어야 하며” 회귀를 드러내는 테스트를 커밋마다 작성하는 워크플로가 필요하다고 설명했습니다 4. 자동화가 판정 입력의 노동은 없애주지만, 재현 스크립트를 항상 준비해 두는 노력은 요구됩니다. Ericsson은 내부 트래커 기준 보고-수정 주기가 평균 142.6시간에서 16.2시간으로 줄었다고 보고했지만, 이는 특정 추적 시스템에서 측정한 경험 수치이며 커밋 세분화와 회귀 테스트 작성이라는 선행 조건에 의존합니다 4.
탐색 종료 후 reset과 결과 해석

탐색이 끝나면 반드시 git bisect reset을 실행해야 합니다. bisect 동안 Git은 candidate 커밋을 해시로 체크아웃하므로 detached HEAD 상태가 됩니다. reset하지 않으면 중간 커밋에 머문 채 작업이 계속됩니다. Nodelog Engineer는 이를 “습관적으로 실행하라”고 안내하며, Pro Git 역시 “최종 reset을 건너뛰면 저장소가 이상한 상태에 남는다”고 경고합니다 12.
git bisect reset에 인자를 전달하면 도착지가 달라집니다.
git bisect reset # 세션 시작 전 HEAD로 복귀
git bisect reset bisect/bad # first bad commit으로 복귀
git bisect reset HEAD # 탐색 중 커밋에 머무름인자 없이 실행하면 탐색 이전 브랜치로 돌아갑니다 2.
출력된 first bad commit은 “변경이 들어간 커밋”이라는 사실만 알려 줄 뿐, “왜” 회귀가 발생했는지는 설명하지 않습니다. 최종 출력은 커밋 해시와 해당 커밋이 수정한 파일 목록을 보여주며, diff를 읽고 원인을 특정하는 일은 사람이 수행해야 합니다 2.
first bad commit이 머지 커밋으로 나올 수도 있습니다. 이 경우 Nodelog Engineer의 안내처럼 그 브랜치 내부를 다시 bisect하여 세부 커밋으로 좁힐 수 있습니다 1.
판정 중 오판에 대한 복구 통로도 있습니다. 10,928개 후보(약 14단계)에서 첫 판정 하나가 나머지 13단계가 탐색할 절반을 결정합니다. 오판은 후반 단계 전체를 잘못된 방향으로 이끌 수 있습니다. 이때는 git bisect log > bisect.log로 판정 이력을 저장하고, 잘못된 라인을 지운 뒤 git bisect reset과 git bisect replay bisect.log를 실행해 탐색을 다시 진행할 수 있습니다 12.
결론: 판정의 품질이 탐색의 정확도를 결정한다
git bisect은 good과 bad 경계를 기준으로 후보를 매 단계 절반씩 줄여, 200개 커밋이면 약 8회 판정으로 first bad commit을 좁히는 도구입니다 1. 그러나 이 결과는 각 판정이 실제로 해당 커밋의 회귀 여부를 반영할 때만 유효합니다.
빌드 실패를 bad로 두면 잘못된 절반이 남고, 스크립트 경로 오타로 exit code 127이 나오면 Git은 모든 커밋을 bad로 착각합니다 2. 따라서 자동화 시에는 빌드 실패를 exit code 125로 분리해 skip 처리하고, 검증 스크립트는 저장소 밖에 두며, 탐색이 끝나면 git bisect reset으로 detached HEAD에서 원래 브랜치로 복귀하는 절차가 결과의 신뢰도를 좌우합니다 42.
good 경계를 넉넉히 잡고 테스트 격리를 포함한 재현 조건을 사전에 준비해 두면, Git이 지목한 커밋 해시와 변경 파일 목록은 “여기서 회귀가 들어왔다”를 가리키는 출발점으로서 충분히 정확합니다 1.
참고자료
- git bisect — 버그가 처음 생긴 커밋 이진 탐색으로 찾기 — Nodelog Engineer · thivelab.com↩
- What Is Git Bisect? Debugging with Binary Search · tms-outsource.com↩
- Common Git commands | GitLab Docs · docs.gitlab.com↩
- ↩
