어제까지 멀쩡하던 기능이 어느 순간 깨져 있다. 언제 깨졌는지는 아무도 모른다. 이런 회귀 버그를 만나면 저는 한동안 커밋 로그를 위아래로 훑으며 눈으로 범인을 찾곤 했다. 커밋이 수백 개 쌓인 브랜치에서는 이 방식이 금방 한계에 부딪힌다. git bisect는 바로 이럴 때 쓰라고 있는 도구다.
정상이던 커밋(good)과 깨진 커밋(bad)만 알려주면 git bisect가 그 사이 구간을 이진 탐색으로 절반씩 좁혀 처음 깨진 커밋(first bad commit)을 찾아준다. 커밋이 1000개라도 로그 밑이 2의 10승이라 열 번 안팎이면 끝난다. 매번 손으로 판정할 필요도 없다. git bisect run에 테스트 스크립트를 물리면 종료 코드를 보고 good/bad를 자동으로 매긴다. 아래에서 수동 방식부터 자동 실행, skip 처리, 마무리까지 차례로 살펴본다.
1. bisect가 하는 일
bisect는 이진 탐색(binary search)에서 온 이름이다. good과 bad 사이의 커밋 구간을 반으로 자르고, 가운데 커밋을 체크아웃해 "여기는 정상인가 깨졌나"를 판정한다. 정상이면 문제는 그 뒤 절반에 있고, 깨졌으면 앞 절반에 있다. 이렇게 매번 후보를 절반으로 줄여 한 커밋만 남을 때까지 반복한다.
전제가 하나 있다. 어느 커밋을 기점으로 정상에서 깨짐으로 한 번만 바뀌어야 한다. 중간에 깨졌다 고쳐졌다를 반복하면 이진 탐색이 엉뚱한 커밋을 지목할 수 있다. 대부분의 회귀 버그는 이 전제를 만족한다.
2. good과 bad 지정
먼저 세션을 시작한다. 지금 HEAD에서 버그가 재현되고, 예전 태그 v1.4.0에서는 멀쩡했다고 하자.
$ git bisect start $ git bisect bad # 현재 HEAD는 깨졌다 $ git bisect good v1.4.0 # 이 태그는 정상이었다 Bisecting: 271 revisions left to test after this (roughly 8 steps) [a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0] Refactor order total calculation
세 줄을 한 줄로 줄일 수도 있다. git bisect start <bad> <good> 순서다. bad가 먼저다.
$ git bisect start HEAD v1.4.0
시작하면 bisect가 알아서 구간 한가운데 커밋을 체크아웃한다. 위 출력의 roughly 8 steps가 앞으로 판정할 횟수다. 271개 후보가 여덟 번이면 끝난다는 뜻이다.
3. 수동 판정으로 감 잡기
자동화에 들어가기 전에 손으로 한두 번 돌려보면 동작이 눈에 들어온다. 체크아웃된 상태에서 빌드하고 테스트해본 뒤, 결과에 따라 good 또는 bad를 입력한다.
$ ./gradlew test --tests OrderTotalTest # 직접 재현 $ git bisect good # 이 커밋에선 통과했다 Bisecting: 135 revisions left to test after this (roughly 7 steps) [f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7] Add coupon stacking rule
한 번 판정할 때마다 후보가 절반으로 준다. 이 과정을 끝까지 손으로 반복해도 되지만, 판정 기준을 명령 하나로 표현할 수 있다면 git bisect run에 맡기는 편이 빠르다.
4. git bisect run 자동 실행
git bisect run은 각 커밋을 체크아웃할 때마다 지정한 명령을 실행하고, 그 종료 코드(exit code)로 good/bad를 판정한다. 종료 코드 규약이 이 도구의 핵심이다.
- 0: good. 현재 커밋은 정상이다.
- 1 ~ 127 (단 125 제외): bad. 현재 커밋은 깨졌다.
- 125: skip. 이 커밋은 판정 불가라 건너뛴다.
- 128 이상: bisect를 중단(abort)한다.
테스트 러너 상당수가 실패 시 1로 끝나므로 별도 가공 없이 바로 물릴 수 있다. 앞의 Gradle 테스트를 그대로 넘겨보자.
$ git bisect run ./gradlew test --tests OrderTotalTest
running './gradlew test --tests OrderTotalTest'
...
Bisecting: 67 revisions left to test after this (roughly 6 steps)
...
running './gradlew test --tests OrderTotalTest'
f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7 is the first bad commit
commit f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7
Author: Jane Kim <jane@example.com>
Date: Mon Sep 14 11:02:33 2026 +0900
Add coupon stacking rule
bisect found first bad commit
마지막에 <sha> is the first bad commit과 커밋 정보가 찍힌다. 여기가 범인이다. 커밋 메시지, 작성자, 변경 파일이 바로 나오니 원인 파악의 출발점으로 삼으면 된다.
테스트 케이스를 새로 짜서 물리기
기존 테스트가 회귀를 잡아주지 못하면, 버그를 재현하는 짧은 스크립트를 따로 만들어 넘기는 방법을 저는 자주 쓴다. 스크립트가 종료 코드 규약만 지키면 무엇이든 된다.
#!/usr/bin/env bash # check.sh — 종료 코드 0이면 good, 1이면 bad set -e curl -sf http://localhost:8080/api/order/total?id=42 \ | grep -q '"total":18000'
$ chmod +x check.sh $ git bisect run ./check.sh
5. 빌드가 깨지는 커밋은 skip
이진 탐색이 지목한 중간 커밋이 하필 빌드조차 안 되는 경우가 있다. 이 커밋을 bad로 세면 판정이 오염된다. 여기서 한 번 걸렸다. 빌드 실패를 곧 버그로 오해해 엉뚱한 커밋을 first bad로 뽑은 것이다.
해결은 빌드 실패를 종료 코드 125로 바꿔 bisect에게 "이 커밋은 건너뛰라"고 알리는 것이다. ||로 연결하면 앞 명령이 실패했을 때만 exit 125가 실행된다.
$ git bisect run sh -c './gradlew compileJava || exit 125; ./gradlew test --tests OrderTotalTest'
빌드가 실패하면 125로 skip, 성공하면 이어서 테스트를 돌려 0/1로 판정한다. 수동으로 세션 도중 특정 커밋만 건너뛰려면 git bisect skip을 쓴다. 범위 표기도 된다.
$ git bisect skip # 지금 커밋 건너뛰기 $ git bisect skip v2.5..v2.6 # 이 구간 통째로 건너뛰기
6. 병합 커밋을 건너뛰는 --first-parent
기능 브랜치를 자주 병합하는 저장소라면, 병합된 브랜치 내부의 중간 커밋들이 탐색 대상에 끼어 판정을 어렵게 한다. 브랜치 내부는 빌드가 반쯤 깨져 있기도 하다. 이럴 때 --first-parent를 주면 병합 커밋의 첫 번째 부모만 따라가며 메인 라인만 탐색한다. 회귀를 끌어들인 병합 커밋 자체를 범인으로 지목하게 된다.
$ git bisect start --first-parent HEAD v1.4.0
7. 세션 마무리, bisect reset
탐색이 끝나면 HEAD가 탐색 도중 커밋에 머물러 있다. 원래 작업하던 위치로 돌아오려면 반드시 git bisect reset으로 세션을 닫아야 한다.
$ git bisect reset Previous HEAD position was f8a9b0c Add coupon stacking rule Switched to branch 'main'
인자 없이 실행하면 bisect start 직전에 있던 커밋으로 되돌아간다. 특정 커밋으로 나가고 싶으면 인자를 준다. bisect가 남긴 refs/bisect/bad 참조로 first bad commit에 바로 체크아웃할 수도 있다.
$ git bisect reset bisect/bad # first bad commit으로 이동해 원인 분석 시작 $ git bisect reset HEAD # 지금 커밋에 그대로 머무름
8. 잘못 판정했을 때 되돌리기
수동으로 돌리다 good을 bad로 잘못 눌렀다면 세션 전체를 버릴 필요는 없다. git bisect log로 판정 이력을 파일에 저장하고, 잘못된 줄을 지운 뒤 git bisect replay로 다시 재생하면 된다.
$ git bisect log > bisect.log $ vi bisect.log # 잘못 찍은 good/bad 줄 삭제 $ git bisect reset $ git bisect replay bisect.log
정리하면, 회귀 버그를 만났을 때 정상 커밋과 깨진 커밋만 짚어주고 판정 기준을 명령 한 줄로 표현할 수 있다면 git bisect run이 나머지를 대신 해준다. 로그를 눈으로 훑는 대신 종료 코드 규약만 지키면 되니, 재현 스크립트 하나 만들어 두는 습관을 들이면 두고두고 쓸 수 있다.