이분 탐색을 활용하는 git bisect로 버그를 도입한 첫 번째 커밋을 찾는 과정을 실제 사례와 데모 리포지토리로 설명한다.
사람들은 면접에서 알고리즘 문제를 배워야 한다고 투덜거린다. 이해한다 — 면접 시스템은 망가져 있다. 그래도 최소한 이분 탐색만큼은 배워둬야 한다.
어쨌든 또 한 번 알고리즘의 실전 적용 사례를 보게 됐다. 이번엔 OG 도구인 git에서다. git bisect - Use binary search to find the commit that introduced a bugref. 그리고 Leetcode는 이것을 알길 원했다 First Bad Version
우리는 회사에서 모노레포를 쓴다. 하루에도 하나의 저장소에서 수백, 많게는 수천 개의 커밋이 만들어진다. 그날, 테스트가 실패하기 시작했고, 로그만으로는 원인 파악이나 디버깅이 충분치 않았다. 실패한 메서드는 특정 역할(role)로 원격 호출을 수행해 테스트 실행에 필요한 토큰을 받아오는 설정 파일에 의존하고 있었다. 어느 순간 그 설정이 바뀌었고 — 문자열 하나가 다른 계정을 참조하도록 수정되면서 — 실패가 발생했다.
어찌어찌 그 나쁜 변경은 통합 테스트를 슬쩍 통과해 버렸다. 며칠 사이에 저장소 전역에서 많은 커밋이 쌓였기 때문에 문제를 일으킨 정확한 파일이나 커밋을 수동으로 찾기는 어려웠다.
그때 같은 문제로 테스트 실패를 겪던 다른 팀의 한 동료가 몇 가지 “마법 같은” 명령을 돌려서, 언제부터 깨지기 시작했는지 정확한 커밋을 재빨리 찾아냈다. 아이디어는 단순하지만 기가 막히다: 정상임이 확실한 커밋 하나와 불량임이 확실한 커밋 하나를 정하고, 이분 탐색으로 실패를 유발한 정확한 커밋을 찾는 것이다.
각 테스트 실행에 시간이 꽤 걸려서 다소 시간이 들긴 했지만, 결국 문제를 도입한 커밋을 정확히 짚어냈다. 그리고 정말로, 그 커밋을 되돌리자 모든 것이 다시 초록불로 돌아왔다.
여기 git bisect가 첫 번째 불량 커밋을 어떻게 찾는지 보여주는 작은 데모 리포지토리가 있다.
File tree:
git-bisect-demo/
├── calc.py # 테스트 대상 라이브러리
├── test_calc.py # calc.add에 대한 pytest 테스트
└── test_script.sh # `git bisect run`에서 사용하는 래퍼
Good version of calc.py (commit where tests pass):
def add(a, b):
return a + b
if __name__ == "__main__":
print(add(2, 3))
Bad version of calc.py (commit that introduced the bug):
def add(a, b):
# 입력이 str로 강제 변환될 때 실수로 문자열 덧셈이 됨
return str(a) + str(b)
if __name__ == "__main__":
print(add(2, 3))
test_calc.py (pytest):
import calc
def test_add():
assert calc.add(2, 3) == 5, "덧셈 실패!"
test_script.sh — git bisect run이 성공 시 종료 코드 0, 실패 시 0이 아닌 값을 반환하도록 사용하는 스크립트:
#!/usr/bin/env bash
set -e
pytest -q
Example commit history (chronological):
git bisect는 이렇게 실행할 수 있다:
git bisect start
git bisect bad HEAD
git bisect good HEAD~9
git bisect run ./test_script.sh
status: waiting for both good and bad commits
status: waiting for good commit(s), bad commit known
Bisecting: 4 revisions left to test after this (roughly 2 steps)
[8dad374fd7c097c4fa3521c0b259e1eefe533520] Commit 5: more changes
running './test_script.sh'
Bisecting: 1 revision left to test after this (roughly 1 step)
[b982ed9373fe235fe61c74b15faf264bc7142398] Commit 3: introduced bug
running './test_script.sh'
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[7b59759ca785572797e04f6b313bb0b735c22529] Commit 2: minor refactor
running './test_script.sh'
b982ed9373fe235fe61c74b15faf264bc7142398 is the first bad commit
commit b982ed9373fe235fe61c74b15faf264bc7142398
Author: Kevin
Date: Sun Nov 2 10:54:47 2025 -0500
Commit 3: introduced bug
calc.py | 10 +---------
1 file changed, 1 insertion(+), 9 deletions(-)
git bisect는 중간 커밋들을 체크아웃하고 ./test_script.sh를 반복 실행해, 테스트를 처음으로 실패하게 만드는 커밋을 찾아낸다.