- GitHub식 PR 리뷰는 원래 변경 위에 수정 커밋을 계속 쌓기 쉬워, 리뷰 과정과 최종 코드 이력이 뒤섞이는 문제가 생김
- 좋은 패치 시리즈는 리팩터링, 새 API 추가, 기존 사용자 마이그레이션처럼 논리적으로 분리된 커밋을 순서대로 읽을 수 있어야 함
- “fix review”나
"minor" 같은 커밋이 쌓이면 변경 의도가 흐려지고, git blame과 git bisect로 문제 원인을 찾는 비용이 커짐
- interdiff 리뷰는 패치 시리즈의 새 버전을 발행하고
git range-diff main..v1 main..v2로 커밋별 변경 차이를 비교함
- 리뷰어는 500줄 전체를 다시 읽지 않고 50줄 증분만 확인할 수 있으며, 작성자는 “address review” 같은 잡음 커밋 없이 최종 커밋 구조를 유지할 수 있음
코드 리뷰 도구와 출발점
- Gerrit Code Review는 Git 저장소와 함께 동작하는 오픈소스 코드 리뷰 도구로, 패치를 제출하고 다른 사람이 코드를 검토하며 코멘트를 남기는 흐름을 지원함
- 코드 리뷰 방식은 Gerrit, GitHub, Phabricator뿐 아니라 버그 트래커에
.patch 파일을 올리는 방식, git send-email, 동료와 긴 논의 끝에 구현을 유도하는 방식까지 다양함
- Go 프로그래밍 언어는 Gerrit을 사용하고, KDE와 LLVM은 Phabricator를 사용했으며, Jujutsu는 Gerrit 사용을 검토한 적이 있음
- Linux 커널은 이메일 기반 워크플로를 특별한 방식으로 사용하고, 많은 프로젝트는 사용이 쉽고 시작 비용이 낮은 GitHub를 선택함
- 핵심 문제는 도구들이 같은 리뷰 모델을 제공하지 않으며, GitHub의 기본 경험이 항상 충분하지는 않다는 점임
좋은 패치 시리즈의 조건
- 이상적인 패치 시리즈는 하나의 큰 변경을 논리적 단계로 나눠 제출하는 형태임
- 먼저 우연히 발견한 코드 정리를 수행함
- 다음으로 새 API를 추가함
- 마지막으로 기존 API 사용자들을 새 API로 마이그레이션함
- 각 패치는 독립적인 이유를 가져야 하고, 순서대로 적용했을 때 코드가 단계적으로 진화하는 모습이 드러나야 함
- 이런 구조는 작성자와 리뷰어뿐 아니라, 나중에
blame이나 bisect로 변경 이유를 찾아야 하는 유지보수자에게도 유리함
- 실제 패치 크기는 상황에 따라 달라짐
- 리팩터링이 없을 수도 있음
- 100줄일 수도 있음
- 핵심 API 변경처럼 모든 호출 지점을 고쳐야 하면 500줄이 될 수도 있음
GitHub식 “diff soup”가 만드는 혼합 이력
- GitHub는 리뷰 코멘트를 반영할 때 원래 커밋 위에 새 커밋을 추가하는 흐름을 명시적·암묵적으로 유도함
- 예시에서는 원래 3개 커밋 뒤에 Alice와 Bob의 리뷰 반영, 테스트 추가,
"minor" 구현 변경 커밋이 차례로 쌓임
- 커밋 그래프에는 부모·자식 관계만 남기 때문에, 리뷰 수정 커밋이 원래 어떤 커밋의 어떤 문제를 고쳤는지 드러나지 않음
- 하나의 “fix review” 커밋이 여러 원래 커밋을 동시에 건드리면 개념 모델이 더 복잡해짐
- 한 수정 커밋이 정확히 하나의 지적사항만 반영하게 만들 수는 있지만, 그만큼 fixup 커밋 수가 늘어남
- 이렇게 큰 변경 묶음과 리뷰 수정이 한 그릇에 섞인 형태가 “diff soup”임
git blame과 git bisect의 신호가 흐려짐
git blame은 줄 단위로 동작하므로, 리뷰 반영 커밋이 기존 줄의 작은 부분만 바꿔도 해당 줄의 출처가 "fix alice review" 같은 커밋으로 보일 수 있음
- 실제 변경 이유를 찾으려면 원래 커밋과 리뷰 수정 커밋을 다시 따라가는 커밋 고고학이 필요해짐
git bisect에서도 "minor" 커밋이 실제 원인인지, 새 API가 처음 추가된 시점부터 있던 버그가 뒤늦게 드러난 것인지 불명확해질 수 있음
- 새 API가 큰 성능 향상을 제공한다면 되돌리기가 바람직하지 않거나 불가능할 수도 있음
- 회귀가 몇 주 뒤에 나타나면, P1 회의 같은 긴급 상황에서
"minor"라는 커밋 메시지만 보고 추가 조사를 해야 하는 비용이 생김
- 리뷰가 두 번만 오간 예시에서도 이력이 복잡해지며, 오픈소스 프로젝트처럼 여러 차례 리뷰가 반복되면 문제는 더 커짐
GitHub UX가 이 흐름을 강화하는 방식
- GitHub에서 증분 리뷰를 하려면 새 커밋을 위에 추가하는 방식이 사실상 가장 쉬움
- 새 API 500줄을 다시 읽는 것보다
+/- 10줄짜리 "fix alice review" 패치를 보는 편이 리뷰어에게 부담이 적음
- PR의 diff 탭은 기본적으로 모든 커밋을 하나의 diff로 합쳐 보여줌
- 이는
git diff master..foo-branch를 보는 것과 비슷함
- 사람들이 변경을 작성하고 읽는 방식과 잘 맞지 않음
- GitHub UX는 이름 붙은 브랜치 모델에 강하게 맞춰져 있어, 브랜치 외의 단위 간 차이를 보기 어렵게 만듦
- force push 후 PR의 “Force Push” 줄에 있는 “Compare” 버튼으로 diff를 볼 수는 있음
- 하지만 이전 브랜치 head와 현재 브랜치 head 사이의 전체 diff만 보여줌
- 더 세밀한 비교를 하려면 커밋 ID를 직접 찾아 URL에 넣어야 함
interdiff 리뷰와 git range-diff
- interdiff 리뷰는 원래 3개 커밋 위에 수정 커밋을 추가하지 않고, 같은 3개 커밋으로 이루어진 새 버전을 발행함
- 예를 들어
v1 패치 시리즈에 Bob의 코멘트를 반영해야 하면, 첫 번째 커밋과 세 번째 커밋을 수정해 v2 시리즈를 만듦
- 이후 Alice의 코멘트, 테스트 추가,
"minor" 변경까지 같은 방식으로 반영해도 최종 결과는 여전히 3개 커밋으로 남음
v1의 커밋 A, B, C가 v2의 X, Y, Z로 진화했다면, git range-diff는 각 위치의 커밋끼리 차이를 보여줌
git range-diff \
main..v1 \
main..v2
- 이 명령은
v1 브랜치의 3개 커밋과 v2 브랜치의 3개 커밋 사이의 쌍별 diff를 보여줌
- 첫 번째 커밋은
diff(A, X)
- 두 번째 커밋은
diff(B, Y)
- 세 번째 커밋은
diff(C, Z)
interdiff 방식의 실무 효과
- GitHub가 보통
diff(main, C) 또는 diff(main, Z)처럼 전체 브랜치 diff를 보여주는 것과 달리, interdiff는 커밋 버전 간 차이를 보여줌
- 리뷰어는 이미 본 500줄짜리 API 변경을 다시 읽지 않고, 수정된 50줄만 확인할 수 있음
- 작성자는 “address review” 같은 잡음 커밋 30개를 히스토리에 쌓지 않아도 됨
git blame은 줄을 더 적은 잡음으로 원래 변경 커밋에 연결할 가능성이 높아짐
git bisect는 회귀를 새 API 커밋 같은 더 의미 있는 단위로 가리킬 가능성이 높아짐
- 결과적으로 기본 진단 도구의 신호대잡음비가 좋아짐
병합 전략과 rebase에 대한 메모
- 패치 병합 전략에 대한 설명은 TODO로 남아 있음
git rebase는 공개 브랜치에 다른 사람이 커밋을 기반으로 쌓을 것으로 기대되는 경우가 아니라면 괜찮다는 입장임
- interdiff 리뷰 시스템은 더 작고 빠르게 병합 가능한 패치를 장려함
- 5개 커밋이 모두 준비될 때까지 기다리지 않아도 됨
- 앞의 3개가 괜찮고 뒤의 2개가 더 필요하면 3개만 병합할 수 있음
- 목표는 사람들이 장기 브랜치가 아니라 main 브랜치를 기반으로 작업하게 만들어, 여러 원격 브랜치가 서로 얽혀 병합되는 상황을 줄이는 것임
- Linux 커널처럼 공개 브랜치를 다른 저장소와 병합하거나 다른 브랜치 위에 명시적으로 공개하는 프로젝트도 있음
- Linux 개발자들은 패치 시리즈를 만들고 다듬을 때
git rebase를 사용할 수 있음
- 하지만 공개 브랜치를 만든 뒤에는 누군가의 트리에 병합될 수 있으므로 force push하지 않고 새 커밋이 있는 새 브랜치를 만듦