Introduction
코드를 짠 결과물이 동일하다고 해서, git history도 동일한 가치를 가지는 것은 아니다. 같은 1000줄 변경이라도 하나의 거대한 commit으로 묶어 push하는 사람과, 의미 단위로 10개의 작은 commit으로 잘라 push하는 사람의 결과물은 협업 관점에서 완전히 다른 행위이다. 늘 협업하는 사람들에게 반복적으로 같은 얘기를 하다 보니, 글로 남겨서 보여주는 게 더 편할 것 같아 정리한다.
이 글에서 강조하고 싶은 takeaway:
GitHub은 Dropbox처럼 “내가 작성한 코드”를 마냥 저장하는 곳이 아니다. “내가 변경한 patch들”을 저장하는 곳이다.
이 관점의 차이를 이해하면 commit, PR, code review, conflict 해결까지 모든 git 활용이 자연스럽게 따라온다.
앞으로 자주 등장할 git 용어 미리보기 (초보자용 한 줄 풀이)
- commit: 변경 사항을 하나의 snapshot으로 저장하는 행위, 그리고 그 snapshot 자체.
- PR (Pull Request): 내 branch의 변경을 main branch에 합쳐 달라고 동료들에게 보내는 요청. review의 단위.
- patch: 한 번의 변경(즉 commit) 자체를 가리키는 표현. 의미상 commit과 거의 동의어.
git revert <sha>: 특정 commit의 변경을 되돌리는 새 commit을 만드는 명령어.git blame <file>: 파일의 각 줄이 어느 commit에서 누구에 의해 마지막으로 수정됐는지 보여주는 명령어.
Patch적 사고란 무엇인가
git의 history는 “파일들의 snapshot 집합”이 아니라 “patch들의 sequence”이다.
git log, git blame, git revert, git cherry-pick, code review.
이 모든 도구는 patch 단위로 동작한다.
따라서 commit과 PR을 만들 때 가져야 할 mental model은 다음과 같다.
- 하나의 commit은 하나의 의도(intent)만을 담는 단위이다.
- 하나의 PR은 하나의 변경 사유(reason for change)를 위한 patch 묶음이다.
- 그 단위를 깨면, git이 제공하는 모든 도구가 함께 약해진다.
LaTeX의 한 줄에 한 문장씩 원칙과 정확히 같은 맥락이다. LaTeX 소스에서 한 문장(혹은 한 구절)이 한 줄이듯이, git에서는 하나의 의미가 하나의 commit이다.
의미론적으로 잘라야 하는 이유
1. Review가 가능해진다
내가 동료의 PR을 review할 때, 가장 막막한 순간은 이런 PR을 받았을 때다.
“Refactor & bug fix & add new feature & format” — 1500 lines changed, 47 files
review어가 이걸 받으면 어디서부터 봐야 할지 막막해진다.
- 이 hunk(변경 덩어리)는 bug fix인가, refactor인가?
- 이 변경은 새 feature 때문인가, 그냥 format 정리인가?
- 이 함수 시그니처가 바뀐 건 의도인가, 실수인가?
같은 변경을 의미 단위로 잘라 commit이 4개라면 이야기가 다르다.
abc1234 Refactor PoseGraph::optimize() to extract residual builder
def5678 Fix off-by-one in keyframe index when num_kf == 0
9876fed Add option to disable robust kernel via YAML
1357ace Format: clang-format on src/optimizer/
각 commit은 읽으면 의도를 알 수 있다. review어는 한 번에 하나씩 집중해서 볼 수 있다. “format” commit은 휙 보고 넘어가고, “fix” commit만 꼼꼼히 보면 된다.
2. Revert가 외과 수술처럼 가능해진다
운영에 배포하고 나서 “어? 왜 keyframe 처리가 이상하지?”가 됐을 때.
거대한 commit이 하나뿐이라면 선택지는 둘뿐이다.
- 1500줄 전체를 revert (refactor도 사라지고, 새 feature도 사라진다)
- 직접 손으로 해당 부분만 골라내서 수정
반면 의미 단위로 잘려 있다면 git revert def5678(특정 commit을 되돌리는 새 commit을 만드는 명령어) 한 줄로 끝난다.
문제가 된 patch만 도려낸다.
만약 이 글을 읽는 사람이 SOTA(State-of-the-Art) approach를 이기기 위해 고군분투하는 연구자라면 이 부분이 더더욱 중요해진다. 어떤 변경을 적용해 봤는데 성능이 안 올랐을 때, 성능이 올랐던 부분까지는 그대로 살려두어야 하는데, patch를 무분별하게 크게 작성하면 도움이 됐던 부분까지 함께 revert되어 버리기 때문이다.
3. git blame이 documentation이 된다
3년 뒤 누가 git blame src/optimizer.cc(파일의 각 줄이 어느 commit에서 마지막으로 수정됐는지 보여주는 명령어)를 했을 때,
- “Big PR #234”라는 commit message만 보이는 코드와
- “Fix off-by-one in keyframe index when num_kf == 0”이라고 보이는 코드는
전혀 다른 정보 가치를 가진다. Commit message는 코드 옆에 영구적으로 붙어 있는 주석이다. 의미 단위로 자르고, 그 의미를 message에 담아주면, 미래의 누군가에게 무료 documentation을 남기는 셈이다.
협업 관점: 큰 push는 충돌을 부른다
이게 patch적으로 사고해야 하는 두 번째 이유이자, 협업 환경에서 더 직접적으로 손해가 발생하는 부분이다.
시나리오: 무엇이 문제인가
A와 B가 같은 repo에서 일하고 있다. A가 feature branch에서 1주일 동안 작업하면서 한 번에 push하지 않고 묵혀뒀다고 하자.
optimizer.cc의 약 절반을 refactorkeyframe.cc에 새 함수 3개 추가config.yaml에 새 option 추가- 그 사이에 main에서 B가
optimizer.cc의 다른 부분을 고쳐서 merge
A가 1주일 후 거대한 PR을 올린다.
이때 일어나는 일:
- Merge conflict가 거의 확정적이다. A가 건드린 영역이 너무 넓어서, B가 어디든 손댔으면 부딪힌다.
- Conflict 해결 자체가 위험하다. A는 이제 1주일 전 자신의 의도와 B의 새 코드를 동시에 이해하면서 손으로 풀어야 한다. 이 과정에서 silent bug가 들어가기 쉽다.
- Review가 뒤로 밀린다. PR이 거대하면 review어가 시간을 잡기 어려워서 PR이 며칠씩 stale 상태로 남는다. 그 사이에 main은 또 움직인다. 악순환이다.
Patch적으로 사고했다면
A가 같은 작업을 patch적으로 잘라서 진행했다면 다음과 같이 됐을 것이다.
| Day | Patch | Push 시점 |
|---|---|---|
| Mon | Refactor optimizer.cc (residual builder 추출) | 그날 PR, 화요일 merge |
| Wed | Add new helper functions to keyframe.cc | 그날 PR, 목요일 merge |
| Thu | Add YAML option for robust kernel | 그날 PR, 금요일 merge |
각 PR은 작고, 의미가 명확하고, review가 빠르다. B의 작업과 부딪힐 영역도 그만큼 좁다. 부딪히더라도 하루치 변경분 안에서의 conflict이므로 해결도 쉽다.
가장 중요한 점: merge가 빨리 되면 main이 곧 내 베이스가 된다. 나의 다음 작업도 최신 main 위에서 시작하므로, 한 명이 한 달치 작업을 묵혀서 만드는 거대한 divergence가 애초에 발생하지 않는다.
안티패턴: “big-bang push”
피해야 할 패턴들을 정리해보자.
그 전에 한 가지 먼저 짚고 넘어가자. 이 모든 안티패턴의 시작점에는 대개 git add .가 있다. 현재 디렉토리의 모든 변경 파일을 한꺼번에 stage하는 이 명령어는 patch를 나누겠다는 의도 자체를 처음부터 막는다. git add .는 인생에서 지워라. 커밋 대상은 항상 눈으로 골라서 올리는 습관이 patch적 사고의 출발점이다.
1. “오늘 작업 끝!” commit
하루 작업이 끝났다는 이유로 모든 변경을 한 commit에 묶는 패턴. 의미 단위가 아니라 시간 단위로 자르는 것은 patch적 사고가 아니다 (다만 혼자 작업하는 research 코드라면 어쨌든 cloud에 백업되는 것 자체가 가장 중요하므로 큰 문제는 아니다).
❌ Today's work
❌ Daily commit
❌ Save progress
이런 commit message는 미래의 본인에게 아무런 정보도 주지 못한다.
2. “이것저것 고쳤음” PR
하나의 PR에 unrelated한 변경 여러 개가 섞여 있는 패턴.
❌ Refactor + bug fix + new feature
이런 PR은 review가 어렵고, 일부만 revert하기도 어렵다. unrelated하면 PR을 분리하라. 가장 단순한 원칙이다.
3. 1주일짜리 long-lived branch
main과 너무 오래 떨어져 있는 branch는 그 자체로 위험 자산이다.
- main에서 다른 사람이 한 변경을 따라가지 못한다.
- merge할 때 conflict가 누적된다.
- review 부담이 누적된다.
원칙: branch는 가능한 한 짧게 살리고, 자주 main에 합쳐라.
4. “내가 다 만들고 한 번에 보여줄게”
거대한 feature를 한 번에 PR로 던지는 것은, review어에게 “이 1500줄을 통째로 OK 하든가, 통째로 reject 하라”고 강요하는 것과 같다. review어가 일부 의견만 제시하기 어려워지고, 결국 두루뭉술한 LGTM으로 끝난다.
큰 feature일수록 더더욱 patch로 쪼개야 한다.
- Feature flag로 가려둔 채 작은 patch들을 미리 main에 머지
- 인터페이스만 먼저 PR로 머지, 구현은 다음 PR
- 의존성이 적은 utility 함수부터 먼저 머지
실전 팁
Commit 단위로 작업하기
코드를 짤 때부터 “이 작업은 몇 개의 commit이 될까?”를 미리 의식한다. 일을 시작하기 전에 머릿속으로 patch 목록을 그려보는 습관을 들이면, 작업 중에도 자연스럽게 commit이 분리되어 만들어진다.
git add -p 활용 & 툴 추천
이미 한꺼번에 많이 고쳤다면, git add -p로 변경된 부분(hunk)을 골라가며 staging해서 의미 단위로 commit을 잘라낼 수 있다.
“전체를 한 번에 add하지 않고, 부분만 골라서 commit할 수 있다”는 사실을 알기만 해도 commit 분리에 대한 부담이 크게 줄어든다.
다만 터미널에서 git add -p를 직접 치는 것보다, lazygit 같은 TUI 클라이언트를 쓰면 훨씬 직관적이다. 파일 목록과 hunk를 한 화면에서 보면서 키 한 번으로 골라 stage → commit → push까지 이어가므로, “patch를 나눠야겠다”는 생각이 들었을 때 실행 비용이 크게 줄어든다. GUI가 편하다면 GitKraken 같은 선택지도 있다. 어느 쪽이든 자기 손에 맞는 툴 하나를 골라두면 patch적 사고가 훨씬 현실적인 습관이 된다.
git rebase -i로 history 정리
push하기 전에 local에서 만든 commit들을 git rebase -i(여러 commit을 squash, 순서 변경, message 수정 등으로 가공할 수 있는 interactive rebase 명령어)로 다듬는 것은 협업자에게 깨끗한 patch를 전달하는 가장 좋은 방법이다.
push 전 history는 정리해도 되는 자산이고, push 후 history는 함부로 건드리면 안 되는 공유 자산이다.
자주 push, 자주 PR
작업이 완벽해질 때까지 기다리지 말고, 의미 단위가 완성되는 즉시 PR을 만든다. “아직 부족한데…“라는 perfectionism이 patch적 사고의 가장 큰 적이다. draft PR로라도 일찍 올려두면 review어가 미리 방향을 잡아줄 수 있고, conflict도 일찍 잡힌다.
정리
| Big-bang push | Patch적 사고 | |
|---|---|---|
| Commit 단위 | 시간/작업량 단위 | 의미 단위 |
| PR 크기 | 큼 (수백~수천 줄) | 작음 (수십~수백 줄) |
| Review | 어렵고, 뒤로 밀림 | 빠르고 집중적 |
| Revert | 통째로 사라지거나 직접 수술 | git revert <sha> 한 줄 |
git blame |
모호한 message만 보임 | 의도가 코드 옆에 영구히 남음 |
| Merge conflict | 자주, 크게, 위험하게 | 드물게, 작게, 안전하게 |
| Long-lived branch | divergence 누적 | 짧고 자주 합쳐짐 |
코드를 짜는 능력만큼이나 patch를 자르는 능력이 협업의 품질을 결정한다. 하나의 commit은 하나의 의도, 하나의 PR은 하나의 사유. 이 단순한 규칙 하나가 review, revert, blame, merge 모든 영역에서 동시에 보상을 준다.
git은 patch들의 sequence이다. 그 patch를 누가 만드는가? 우리다. 좋은 patch를 만드는 사람이 git을 잘 쓰는 사람이다.
대학원생을 위한 Git 시리즈입니다.