Git bisect 실전: 자동 테스트로 첫 회귀 커밋을 찾고 안전하게 돌아오기
정상과 실패가 확인된 커밋 사이에서 회귀 지점을 좁히는 개발자 실습입니다. 격리 저장소의 자동 테스트로 종료 코드와 검사 불가 판정을 구분하고 로그 보존, 환경 진단, 원래 상태 복귀를 다룹니다.
어제까지 통과하던 테스트가 오늘 실패했다면 모든 커밋을 순서대로 읽는 것보다 ‘어디서부터 실패했는가’를 먼저 좁히는 편이 빠릅니다. Git bisect는 정상으로 확인한 커밋과 실패하는 커밋 사이를 나누어 조사하고, 같은 검사를 반복해 첫 실패 커밋을 찾습니다. 유용한 자동화의 조건은 복잡한 명령이 아니라 일관된 판정입니다. 빌드 환경 고장을 제품 회귀로 잘못 판정하면 도구는 정확한 속도로 엉뚱한 범인을 찾습니다.
시작 조건
정상 커밋 하나와 실패 커밋 하나, 그 차이를 재현하는 테스트가 필요합니다. 추적하려는 현상은 조사 구간에서 대체로 정상에서 실패로 바뀌는 형태여야 합니다. 고장과 수정이 여러 번 반복된 구간이라면 조사 범위를 더 좁히고 찾은 커밋의 앞뒤를 재검증하세요. ‘첫 실패’는 지정한 범위와 판정 기준 안에서의 결과이지 작성자의 책임을 입증하는 말이 아닙니다.
macOS·Linux POSIX 셸, Git, Python 3를 전제로 합니다. Windows에서는 Git Bash와 사용 가능한 Python 명령을 준비하거나 WSL에서 동일한 블록을 실행합니다. 아래는 작성일 2026년 10월 1일 macOS에서 격리한 새 저장소로 실제 실행한 예제입니다. 사용 중인 프로젝트나 원격 저장소는 변경하지 않았습니다.
설치 확인
git --version
python3 --versionGit이 없으면 공식 설치 페이지에서 운영체제에 맞는 배포판을 선택합니다. macOS의 git 실행은 개발 도구 설치 안내를 띄울 수 있으니 설치가 끝난 뒤 다시 버전을 확인합니다. Linux 패키지 관리자의 설치 명령은 배포판마다 다릅니다. 이 글은 설치 관리자를 실행해 검증한 것이 아니라 공식 안내를 확인했고, 이미 설치된 실행기를 실습에 사용했습니다. 관리자 권한이 필요한 설치와 아래의 일반 사용자 실습을 구분하세요.
실무 저장소에서는 git status --short로 먼저 작업 상태를 확인합니다. bisect는 과거 커밋을 체크아웃하기 때문에 저장하지 않은 수정과 충돌할 수 있습니다. 변경을 별도 커밋이나 확인한 백업으로 보존한 뒤 시작하세요. 저장소의 오래된 테스트 코드도 실행되는 만큼 출처를 신뢰할 수 있는 프로젝트에서만 진행합니다.
최소 실습 만들기
다음 블록은 임시 폴더 안에 정상 커밋, 무관한 변경, 버그 도입, 또 다른 무관한 변경을 만듭니다. 사용자 이름과 이메일은 이 실습 저장소에만 설정하므로 전역 설정을 바꾸지 않습니다. Git 서명과 훅의 개입을 피하는 설정도 이 새 저장소 안으로 제한했습니다.
DEMO_DIR=$(mktemp -d)
mkdir "$DEMO_DIR/repo" "$DEMO_DIR/empty-hooks"
cd "$DEMO_DIR/repo"
git init -q
git config user.name 'Bisect Demo'
git config user.email '[email protected]'
git config commit.gpgsign false
git config core.hooksPath "$DEMO_DIR/empty-hooks"
printf 'def total(price, count):\n return price * count\n' > calc.py
git add calc.py
git commit -qm 'correct multiplication'
git tag demo-good
printf 'first unrelated change\n' > notes.txt
git add notes.txt
git commit -qm 'add notes'
printf 'def total(price, count):\n return price + count\n' > calc.py
git add calc.py
git commit -qm 'introduce arithmetic regression'
git tag demo-regression
printf 'second unrelated change\n' >> notes.txt
git add notes.txt
git commit -qm 'extend notes'
git tag demo-bad
cat > "$DEMO_DIR/check.py" <<'PY'
import sys
from pathlib import Path
if not Path("calc.py").exists():
sys.exit(125)
from calc import total
sys.exit(0 if total(10, 3) == 30 else 1)
PY테스트 파일을 저장소 바깥에 두는 이유는 과거 커밋으로 이동하더라도 검사기 자체가 사라지거나 바뀌지 않도록 하기 위해서입니다. calc.py는 체크아웃된 시점의 파일을 읽습니다. 아래의 -B와 PYTHONPATH 설정은 이 작은 실습에서 바이트코드 파일을 만들지 않고 현재 저장소를 import 경로로 사용하는 역할을 합니다.
자동으로 찾기
git bisect start demo-bad demo-good
git bisect run env PYTHONPATH=. python3 -B "$DEMO_DIR/check.py"
git bisect log > "$DEMO_DIR/bisect.log"
FOUND=$(git rev-parse refs/bisect/bad)
EXPECTED=$(git rev-parse demo-regression)
test "$FOUND" = "$EXPECTED" && printf 'matched regression commit\n'
git show --stat "$FOUND"
git bisect reset성공 신호는 Git의 first bad commit 메시지와 matched regression commit 출력입니다. 제목 introduce arithmetic regression이 표시되어야 합니다. 커밋 ID는 작성 시각 등에 따라 달라지므로 기사에 고정 해시를 복사해 비교하지 않습니다. 마지막 reset은 bisect 시작 전 체크아웃 위치로 돌아갈 뿐이며 찾은 커밋을 삭제하거나 버그를 수정하지 않습니다.
업무 테스트로 바꾸기
실제 작업에서는 demo-bad와 demo-good 대신 재현한 실패 커밋과 확인한 정상 릴리스 태그를 사용하고, check.py를 해당 결함만 검증하는 검사기로 바꿉니다. 처음과 끝 커밋에서 같은 검사기가 각각 정상과 실패를 반환하는지 먼저 확인해야 합니다. 전체 테스트 묶음보다 회귀를 직접 드러내는 작은 검사가 진단을 쉽게 합니다.
공식 문서상 bisect run은 종료 코드 0을 정상, 1~127 중 125를 제외한 값을 실패, 125를 검사 불가능으로 처리합니다. 그 밖의 코드는 탐색을 중단합니다. 명령을 찾을 수 없을 때의 127도 실패 판정 영역에 들어가므로 실행기 존재 여부를 먼저 확인해야 합니다. 환경 장애를 아무렇게나 1로 반환하는 래퍼는 피하세요.
구버전에서 필수 파일이 없는 것처럼 정말 검사할 수 없는 커밋만 125로 건너뜁니다. 원하는 결과가 안 나온다는 이유로 skip을 쓰면 안 됩니다. 범인 근처 커밋을 건너뛰면 첫 실패가 하나로 확정되지 않을 수 있으며 이때는 후보 목록을 결과로 보고 환경을 복구해 추가 조사합니다.
주요 설정
문제의 위치를 확실히 아는 경우 공식 문서의 start 뒤 --와 경로 지정으로 범위를 제한할 수 있습니다. 그러나 의존성 잠금 파일이나 빌드 설정에서 발생한 회귀를 놓칠 수 있으니 처음부터 경로를 지나치게 좁히지 않는 것이 좋습니다. 성능 변화처럼 good/bad가 어색한 경우 --term-old와 --term-new로 fast/slow 등 다른 용어를 지정할 수 있습니다. 이 옵션들은 공식 문서에서 확인했으며 이번 로컬 실행은 기본 good/bad 자동 탐색에 한정합니다.
벤치마크를 판정기로 삼는다면 반복 실행, 고정된 하드웨어와 부하, 잡음보다 큰 임계값을 준비하세요. 네트워크 응답과 현재 시각에 따라 바뀌는 테스트는 커밋 차이보다 환경 차이를 잡을 수 있습니다. 의존성도 당시 잠금 파일을 기준으로 복원하되 오래된 설치 스크립트가 어떤 권한을 요구하는지 검토합니다.
권한과 비용
Git bisect 자체는 로컬 커밋을 조사하며 이 실습은 원격 통신, 토큰, 유료 서비스가 없습니다. 하지만 bisect run으로 지정한 테스트는 일반 셸 명령과 똑같은 권한을 가집니다. 과거 코드의 빌드 스크립트가 네트워크로 자료를 보내거나 비밀 환경 변수를 읽을 수 있으므로 민감한 자격증명이 없는 격리 환경에서 실행하세요. 이 도구는 보안 샌드박스가 아닙니다.
프로젝트의 테스트가 클라우드 자원을 만들거나 API를 호출하면 해당 비용은 별도로 발생합니다. 배포·결제·운영 데이터 수정까지 하는 통합 테스트를 판정기로 직접 쓰지 말고 가짜 데이터와 로컬 재현으로 바꾸는 것이 안전합니다. 원격 로그에 내부 소스나 고객 자료를 올릴 때는 별도 데이터 정책을 적용해야 합니다.
오류 진단
체크아웃이 거절되면 작업 파일을 덮어쓰려 하지 말고 git status를 보고 보존할 변경을 먼저 확인합니다. ModuleNotFoundError가 나오면 현재 폴더가 실습 repo인지, PYTHONPATH=.가 전달되었는지 확인하세요. AssertionError나 예외가 제품 결함인지 환경 문제인지 구분하지 않으면 일반 실패 종료 코드가 자동 판정에 들어갑니다.
판정을 잘못 입력했을 때는 git bisect log를 파일로 저장하고 잘못된 항목을 검토한 뒤 reset과 replay로 다시 진행할 수 있습니다. replay는 공식 문서 기준이며 이번 실습에서는 로그 저장과 reset까지 실행했습니다. 모든 정상·실패 표시를 기억에 의존해 다시 입력하는 것보다 검토 가능한 기록을 남기는 편이 낫습니다.
종료와 복구
중단하거나 완료했다면 git bisect reset을 실행하고 git status --short, git branch --show-current로 돌아온 상태를 확인합니다. reset은 추적하지 않는 파일이나 테스트가 외부에 만든 자원을 정리해 주지 않습니다. 실습 폴더 삭제는 repo 밖으로 나온 뒤 DEMO_DIR의 실제 경로를 확인하여 그 임시 폴더만 대상으로 수행하세요. Git 전역 설정과 시스템 설치는 변경하지 않았으므로 되돌릴 필요가 없습니다.
범인 후보를 찾은 뒤에도 그 커밋 바로 전과 해당 커밋에서 검사를 다시 실행하고 실제 diff를 읽어야 합니다. 자동 이분 탐색은 조사 범위를 줄이는 장치이지 원인 설명을 대신하는 판결문이 아닙니다. 수정과 회귀 테스트를 별도 변경으로 남기는 것이 다음 장애를 줄이는 마지막 단계입니다.