8P by GN⁺ 18시간전 | ★ favorite | 댓글 2개
  • 이진 탐색(binary search) 개념은 면접 문제뿐 아니라 실제 개발 도구인 Git에서도 활용됨
  • 대규모 monorepo 환경에서 테스트가 갑자기 실패했을 때, 로그만으로는 원인을 추적하기 어려운 상황이 발생
  • 한 동료가 좋은 커밋과 나쁜 커밋을 지정해 git bisect로 자동 탐색을 수행해 버그가 시작된 문제 커밋을 정확히 찾아냄
  • 각 단계에서 스크립트를 실행해 테스트 결과에 따라 커밋을 자동 분류하며, 첫 번째로 실패한 커밋을 식별
  • 이진 탐색 원리를 활용한 git bisect대규모 코드베이스에서 버그 원인을 신속히 추적하는 강력한 도구

알고리듬과 실제 사례

  • 이진 탐색(binary search) 알고리듬은 단순한 면접 문제를 넘어 실제 디버깅 도구에서도 핵심 원리로 작동함
  • git bisect는 “버그를 처음 도입한 커밋(first bad commit)”을 찾기 위해 이진 탐색을 사용하는 도구”로 사용 가능
    • Leetcode의 “First Bad Version” 문제와 유사한 원리로 작동함

실제 업무 환경에서의 문제 상황

  • 대규모 monorepo를 사용하는 환경에서는 수백~수천 개의 커밋이 하루에도 발생함
  • 특정 테스트 실패의 원인을 로그만으로는 추적하기 어려움
  • 실패 원인은 원격 호출에 필요한 토큰을 얻는 설정 파일의 문자열 변경으로, 다른 계정을 참조하게 되어 테스트가 실패함
  • 이 변경은 통합 테스트를 통과했지만 실제로는 문제를 일으켰고, 수많은 커밋 중 어느 시점에서 발생했는지 찾기 어려웠음

git bisect를 이용한 문제 해결

  • 다른 팀의 동료가 git bisect 명령어를 사용해 문제 커밋을 빠르게 식별
    • 좋은 커밋(good)나쁜 커밋(bad) 을 지정한 후 자동으로 중간 커밋들을 체크아웃하며 테스트를 수행하고 원인을 좁혀감
    • 각 테스트 실행은 시간이 걸렸지만, 결국 정확히 문제를 도입한 커밋을 찾아냄
    • 해당 커밋을 되돌리자 테스트가 모두 정상으로 복구됨

git bisect 실행 과정

  • 예시 커밋 히스토리
    • Commit 1: 초기 커밋 (정상)
    • Commit 2: 리팩터링 (정상)
    • Commit 3: 버그 도입 (오류 발생)
    • Commit 4~10: 비기능적 변경 (오류 유지)
  • 실행 명령 예시
    git bisect start  
    git bisect bad HEAD  
    git bisect good HEAD~9  
    git bisect run ./test_script.sh  
    
  • 테스트 스크립트(test_script.sh)는 성공 시 0, 실패 시 비정상 코드를 반환
  • git bisect는 중간 커밋을 자동으로 체크아웃하고 테스트 스크립트를 실행하며,
    테스트 실패 시점을 기준으로 첫 번째 나쁜 커밋을 식별
  • 출력 결과에서 b982ed9373fe235fe61c74b15faf264bc7142398 커밋이 첫 번째 버그 커밋으로 확인됨

결론

  • git bisect이진 탐색 원리를 코드 히스토리 탐색에 적용한 실용적 도구
  • 대규모 저장소나 복잡한 변경 이력에서도 버그 도입 시점을 신속히 추적할 수 있음
  • 테스트 자동화와 결합하면 대규모 코드베이스에서도 안정적 디버깅이 가능함

이런 문제들 때문에 TBD(trunk-based-develop)을 사용합니다.

Hacker News 의견
  • 예전에 테스트 커버리지도 없고 추상화도 엉망인 거대한 코드베이스에서 일할 때, git bisect는 거의 유일하게 쓸 만한 도구였음
    코드가 너무 복잡해서 논리적으로 버그를 추적하는 게 불가능했기 때문에, 어떤 커밋에서 문제가 생겼는지를 찾는 게 훨씬 쉬웠음
    하지만 품질이 높은 코드베이스에서는 굳이 bisect가 필요하지 않았음. 각 컴포넌트를 독립적으로 테스트할 수 있고, 관측성(observability) 도 잘 되어 있어서 어디를 봐야 할지 명확했음

    • git bisect는 절대 불필요하지 않다고 생각함. 단순히 버그를 찾는 것뿐 아니라 그 버그가 생겼는지를 이해하게 해줌
      커밋 메시지가 충실한 프로젝트라면, bisect를 통해 과거 커밋의 맥락을 파악하고 그 내용을 버그 수정 커밋에 반영할 수 있음. 이런 순환이 커밋 문화 자체를 강화시킴
    • 예전에 OSS 프로그램에서 이상한 문자열이 들어간 버그를 찾은 적이 있음. C 코드였고 초기화되지 않은 변수 때문이었음
      직접 추적은 불가능했지만, bisect 스크립트를 작성해 30분 정도 돌리니 문제의 커밋을 정확히 찾아냈음
    • git bisect는 원래 Linux 커널 회귀(regression) 를 찾기 위해 도입된 도구였음
      하드웨어 드라이버처럼 테스트가 불가능한 경우에도, 일반 사용자가 직접 커널을 bisect해서 문제 커밋을 특정할 수 있게 되었음
      예전에는 이메일로 개발자에게 도움을 요청해야 했지만, 이제는 사용자가 스스로 문제를 좁혀갈 수 있게 되었음
    • 단순히 버그를 고치는 게 목적이라면 bisect가 필요 없을 수도 있음. 하지만 버그가 언제부터 존재했는지 알아야 할 때가 있음
      예를 들어 잘못 처리된 데이터의 범위를 추적하거나, “이게 버그인가 기능인가”를 판단할 때 유용함
    • 때로는 버그가 언제 수정되었는지 알아야 할 때도 있음
      예를 들어 고객이 6년 전 버전에서 문제를 겪고 있을 때, 4년 전 버전으로 업그레이드하면 해결되는지 확인할 수 있음
      혹은 코드가 크게 리팩터링되었을 때, 버그 수정이 의도적이었는지 우연이었는지도 파악 가능함
  • git bisect는 잘 작동할 때는 훌륭하지만, 모든 버그를 찾을 수 있는 건 아님
    어떤 버그는 도입 당시에는 증상이 없고, 나중에 다른 변경으로 인해 드러나기도 함
    이런 경우 bisect의 전제(좋은 커밋과 나쁜 커밋 사이에 단 한 번만 버그가 등장함)가 깨짐
    테스트 불가능한 커밋은 skip할 수 있지만, 그게 문제 커밋이면 결과가 애매해짐

  • 최근 처음으로 진지하게 git bisect를 써봤는데, 거의 마법 같음
    두 개의 동일한 이름의 함수가 있었고, 코드 포매팅 작업 중에 올바른 함수의 import가 제거되면서 문제가 발생했음
    여러 번 코드를 검토했지만 bisect로 문제 커밋을 특정하기 전까지는 원인을 전혀 몰랐음

  • 나는 보통 버그가 생긴 파일이나 함수의 범위를 이미 알고 있어서 bisect를 자주 쓰진 않음
    대신 git log -L :func_name:path/to/file.c 명령으로 특정 함수의 변경 이력을 추적함
    .gitattributes 설정이 필요함

    • .gitattributes 설정이 궁금하다는 질문이 있었음. 어떤 내용이 필요한지 더 알고 싶다는 반응이었음
    • 매일 bisect를 쓴다는 사람도 있었음. 워크플로우가 완전히 다름
    • C++처럼 다형성 함수가 있는 경우에는 git log -L이 약함. 동일한 이름의 오버로드 함수 중 특정 버전을 추적하기 어렵기 때문임
    • .gitattributes가 없다면 git log -S로 특정 문자열이 포함된 커밋을 찾는 것도 방법임
  • 테스트 스크립트에서 exit code 125를 알아두면 좋음
    빌드 실패처럼 테스트 결과를 판별할 수 없는 경우, 125를 반환하면 bisect가 해당 커밋을 건너뜀
    관련 내용을 내 블로그 글에 정리했음

    • 병합 커밋이 CI 통과 지점을 의미하는 저장소에서는 git bisect --first-parent를 쓰면 유용함
      이렇게 하면 “어떤 PR이 버그를 도입했는가”를 빠르게 찾을 수 있고, 이후 해당 브랜치에서 세부 bisect를 한 번 더 돌리면 됨
  • 플레이키 테스트(flaky test) 가 생겼을 때 bisect가 빛을 발함
    레이스 컨디션으로 인해 테스트를 수십만 번 돌려야 확신할 수 있는 경우, bisect 스크립트를 백그라운드로 돌려두면 현실적으로 해결 가능함

    • 이런 경우에는 베이지안 이진 탐색을 적용하면 테스트 횟수를 훨씬 줄일 수 있을 것 같음
  • 최근 Svelte로 만든 음악 플레이어 프로젝트(lets-make-sweet-music.com)에서 bisect로 버그 원인을 찾았음
    테스트도 없고, 오류 로그도 없었는데, dependabot 업데이트로 커밋이 많아져서 추적이 어려웠음
    bisect 덕분에 문제의 커밋을 찾아냈고, 원인은 내가 교체한 파일이 이벤트 다중 바인딩 기능을 구현하지 않았던 것임
    커밋을 작게 유지하면 bisect로 찾은 문제의 원인을 빠르게 좁힐 수 있음

  • 누군가 “면접에서 이진 탐색(binary search) 을 배워야 한다는 건 억지”라고 했는데, git bisect는 그 개념을 실제로 보여주는 좋은 예임
    하지만 직접 구현할 필요는 없음. 대부분의 언어에는 이미 표준 라이브러리로 제공됨

    • 흥미롭게도, 이진 탐색이 1940년대에 처음 제안됐지만 버그 없는 구현이 1960년대에야 나왔다는 이야기가 있음
      중간 인덱스를 계산할 때 (low + high) / 2로 하면 오버플로가 생길 수 있음
    • 개인적으로는 모든 개발자가 한 번쯤 임의 정밀도 정수 언어(예: Python)로 이진 탐색을 직접 구현해보는 게 좋다고 생각함
      불변식(invariant) 기반 사고를 훈련하는 최고의 연습임
  • Git에는 bisect 외에도 log -L, log -S, blame 같은 훌륭한 코드 탐색 도구가 있음
    예전에 이 주제로 블로그 글을 쓴 적이 있음