Git은 매일 쓰지만 워크플로우 결정의 근거는 자주 흐릿하다. “rebase가 머지보다 commit history를 깨끗하게 만든다"는 말은 들어봤어도, 그게 정확히 무슨 뜻인지 언제 쓰면 안 되는지는 한 번 정리하지 않으면 흐릿하게 남는다.

GitHub의 PR과 Code Review는 후속 글로 분리한다.

Commit

Git의 모든 것은 commit이다. 한 commit은 그래프의 한 노드이고, 직전 commit을 parent로 가리켜 commit history를 형성한다. branch는 그 노드들 위에 그어지는 이름표일 뿐이고, merge는 두 흐름이 만나는 지점이다.

flowchart LR
    A((A)) --> B((B)) --> C((C))
    C --> D((D))
    C --> E((E))
    D --> F((F))
    E --> F

이 그래프 모양이 이후 디버깅의 비용을 결정한다. git bisect로 회귀를 찾을 때, git blame으로 의도를 추적할 때, 깔끔한 commit 단위는 검색 공간을 줄여준다.

commit hygiene

좋은 commit은 한 가지 의도를 갖는다. 새 기능 추가와 무관한 리팩터링을 같은 commit에 섞으면, 나중에 둘 중 하나만 되돌리고 싶을 때 어쩔 수 없이 손으로 분리해야 한다.

메시지는 그 의도를 설명한다. 제목은 50자 이내로, 본문이 필요하면 빈 줄을 두고 why를 적는다. what은 diff가 이미 보여주므로, 메시지는 그 변경이 왜 필요했는지를 남기는 게 가치 있다.

Conventional Commits 같은 컨벤션은 feat:, fix:, chore: 접두사로 메시지의 종류를 분류한다. 도구가 자동으로 changelog를 만들거나 semantic version을 결정하는 흐름과 잘 맞는다.

Branch

branch는 commit을 가리키는 이름표다. 새 commit을 만들면 현재 branch가 가리키는 곳이 새 commit으로 한 칸 이동한다. git checkout은 HEAD를 다른 branch로 옮기는 동작이고, 그뿐이다. 디스크에 별도 디렉토리가 만들어지거나 무거운 작업이 일어나는 게 아니다.

이 단순함이 branch strategy를 가능하게 한다.

branching strategy

세 가지 패턴이 자주 비교된다.

전략흐름적합 상황
trunk-based짧은 feature branch (수 시간~수 일), 자주 main 머지CI/CD 성숙, 빠른 배포
GitHub Flowfeature branch → PR → main 머지 → 즉시 배포웹 서비스, continuous deployment
GitFlowmain / develop / feature / release / hotfix 다층 분리릴리스 주기가 길고 버전 관리가 엄격한 환경

trunk-based가 단순하고 통합 빈도가 높아 머지 충돌이 적은 반면, GitFlow는 릴리스 통제가 강하지만 branch가 많고 운영 비용이 크다. GitHub Flow는 그 중간으로, 대부분의 SaaS 워크플로우에 잘 맞는다.

선택의 핵심은 “main에 얼마나 자주 통합할 수 있는가"다. 통합이 잦을수록 그래프가 단순해지고 충돌이 작아진다.

merge vs rebase

두 흐름을 합칠 때 Git은 두 가지 옵션을 준다.

flowchart TB
    subgraph Before ["분기 상태"]
        m1((m1)) --> m2((m2)) --> m3((m3))
        m1 --> f1((f1)) --> f2((f2))
    end
    subgraph Merge ["merge 결과"]
        mm1((m1)) --> mm2((m2)) --> mm3((m3)) --> mc((merge))
        mm1 --> mf1((f1)) --> mf2((f2)) --> mc
    end
    subgraph Rebase ["rebase 결과"]
        rm1((m1)) --> rm2((m2)) --> rm3((m3)) --> rf1((f1')) --> rf2((f2'))
    end

merge는 두 commit history를 합치는 새 commit(merge commit)을 만든다. 그래프에 분기와 합류가 그대로 남아 “언제 누가 어디서 합쳐졌는지"가 보인다.

rebase는 한쪽 commit들을 다른 쪽 끝에 다시 쌓는다. 그래프가 linear해지지만 commit hash는 모두 새로 만들어진다(f1f1'). 본질적으로 다른 commit이라는 뜻이다.

선택의 기준은 분명하다. 공유된 commit을 rebase하지 말 것. 동료가 이미 그 commit을 자기 branch에 갖고 있다면, rebase 후 force push는 그 동료의 commit history를 깨뜨린다. 내 로컬 branch나 아직 공유 전인 feature branch라면 rebase로 commit history를 정리해도 안전하다.

main 자체의 정책은 팀 컨벤션이다. linear history를 선호하면 rebase 또는 squash, 머지 흐름이 보존되길 원하면 merge commit. 정답은 없고, 한 번 정하면 일관되게 유지하는 게 더 중요하다.

흔한 함정

git push --force 가 동료의 commit을 덮어쓴다

shared branch에 force push하면 그 시점 이후의 동료 commit이 사라질 수 있다. --force-with-lease를 쓰면 remote가 내가 본 상태와 다를 때 거부되므로 안전판이 된다.

rebase 중 매 commit마다 충돌

rebase는 commit을 하나씩 다시 쌓는 방식이라, 충돌도 commit 단위로 발생한다. 한 번에 다 해결하고 싶다면 일단 merge로 처리하거나, 또는 rebase가 끝난 뒤 git rebase -i로 commit을 squash해 다시 다듬는 흐름이 효율적이다.

git reset --hard로 잃은 작업

reset --hard는 working tree와 index를 함께 초기화한다. commit된 작업은 reflog에 남아 있어 git reflog로 SHA를 찾고 git reset --hard <sha>로 복구할 수 있다. commit조차 안 한 변경은 복구가 어려우므로, 위험한 작업 전에 임시 commit을 만들어두는 습관이 안전판이 된다.

merge commit이 그물망처럼 쌓인다

작은 feature branch가 자주 main에 머지되면 그래프가 깔끔하지만, long-lived branch끼리 merge가 반복되면 그물망 모양이 된다. 이때는 squash merge나 rebase merge로 정리하거나, 애초에 long-lived branch 수를 줄이는 쪽으로 워크플로우를 바꾸는 게 근본 해결이다.

정리

Git 워크플로우의 결정들은 결국 그래프 모양에 대한 선택이다.

  • commit: 한 가지 의도, 메시지에 why
  • branch: 단지 commit 포인터, 통합 빈도가 strategy를 결정
  • merge vs rebase: 공유된 commit은 건드리지 않기, 그 외에는 팀 컨벤션

결국 commit hygiene, branch strategy, merge·rebase 선택이 모여 그래프의 모양이 된다. 그 모양이 단순할수록 이후 디버깅과 협업의 비용이 작아진다.

다음 편은 이 그래프를 GitHub PR로 어떻게 협업하는가 — Code Review 사이클과 머지 전략이다.