병합이 필요한 순간
기능 브랜치에서 작업이 완료되면 그 변경 사항을 메인 브랜치에 통합해야 한다. 이것이 병합(merge)이다.
병합은 두 브랜치가 분기된 이후 각자에서 발생한 변경 사항을 하나의 스냅샷으로 결합하는 작업이다. Git은 이를 자동으로 처리하기 위해 공통 조상(merge base)을 찾는다. 두 브랜치가 마지막으로 같은 커밋을 가리키던 시점이 공통 조상이다.
병합 알고리즘은 공통 조상의 스냅샷, 한쪽 브랜치의 최신 스냅샷, 다른 쪽 브랜치의 최신 스냅샷 — 이 세 개의 스냅샷을 비교해 충돌 없이 합칠 수 있는지 판단한다. 같은 파일의 같은 줄이 양쪽에서 다르게 수정된 경우에만 충돌이 발생한다.
Fast-forward 병합 — 포인터만 이동
가장 단순한 병합 형태다. 한쪽 브랜치가 다른 쪽의 직선 히스토리상에 있을 때, 새 커밋 없이 포인터만 앞으로 이동한다.
상황: main이 커밋 B를 가리키고, feature가 B→C→D로 세 커밋 앞서 있다. feature에서 main을 분기한 이후 main에는 새 커밋이 없다. 이 경우 git merge feature를 실행하면 Git은 "main을 D로 이동하면 feature의 모든 이력을 포함할 수 있다"고 판단해 포인터만 갱신한다.
Fast-forward 이후의 커밋 그래프에는 병합 커밋이 존재하지 않는다. 히스토리는 완전히 선형이다. 병합이 일어났다는 사실 자체가 그래프에서 사라진다.
# 항상 병합 커밋을 만들고 싶다면 (기록 보존) git merge --no-ff feature # 반드시 fast-forward만 허용 (아니면 실패) git merge --ff-only feature
--no-ff). PR이 언제 병합되었는지를 그래프에서 명확히 볼 수 있도록 하기 위해서다. "Squash and merge", "Rebase and merge" 등 다른 전략도 선택할 수 있다.3-way 병합 — 병합 커밋 생성
두 브랜치가 공통 조상 이후 각자 새 커밋을 만들었다면 fast-forward가 불가능하다. 이때 Git은 세 개의 스냅샷을 비교해 새로운 병합 커밋을 생성한다.
병합 커밋은 두 개의 parent를 가지는 특별한 커밋이다. 커밋 오브젝트에 parent 줄이 두 개 있다. 이 커밋이 그래프에 합류점으로 남아 "이 두 라인이 여기서 합쳐졌다"는 사실을 영구히 기록한다.
3-way 병합 알고리즘의 동작 원리: 공통 조상(O), main의 최신 커밋(A), feature의 최신 커밋(B)을 비교해 각 파일의 각 줄에 대해 다음 규칙을 적용한다.
충돌이 발생하면 Git은 충돌 마커(<<<<<<<, =======, >>>>>>>)를 삽입한 파일을 작업 디렉터리에 남기고, 스테이징 영역에 충돌 상태를 기록한다. 사용자가 파일을 편집해 충돌을 해소하고 git add한 뒤 git commit하면 병합이 완료된다.
git merge feature # 충돌 발생 시 중단됨 git status # 충돌 파일 확인 # 충돌 파일을 편집기로 열어 <<<, ===, >>> 마커 해결 git add src/conflicted.ts git commit # 병합 커밋 완성 # 또는 병합 취소하려면 git merge --abort
병합 시각화
병합 전략 선택하기
프로젝트와 팀의 워크플로에 따라 병합 전략을 의도적으로 선택해야 한다. 각 전략의 트레이드오프를 이해하자.
병합 커밋(--no-ff): 브랜치의 존재와 병합 시점이 그래프에 명확히 남는다. PR 단위의 작업 구분이 가능하고, git revert로 PR 전체를 한 번에 되돌릴 수 있다. 단, 그래프가 복잡해 보일 수 있다.
Fast-forward(--ff-only): 선형 히스토리를 유지해 git log가 읽기 쉽다. 단, 브랜치가 언제 만들어지고 병합되었는지를 그래프만으로는 알 수 없다. 단독 작업이나 짧은 수명의 브랜치에 적합하다.
Squash merge: 브랜치의 모든 커밋을 하나로 압축해 main에 추가한다. 기능 브랜치의 지저분한 WIP 커밋들을 숨기고 main 히스토리를 깔끔하게 유지할 수 있다. 단, 세부 작업 내역이 사라지므로 이진 탐색(git bisect) 시 단위가 커진다.
Octopus 병합과 재귀 병합
대부분의 병합은 두 브랜치 사이에서 일어나지만, Git은 세 개 이상의 브랜치를 동시에 병합하는 옥토퍼스 병합도 지원한다.
git merge branch1 branch2 branch3처럼 여러 브랜치를 나열하면 한 번의 명령으로 병합할 수 있다. 결과 커밋은 세 개 이상의 parent를 갖는다. 독립적인 기능 브랜치들을 릴리스 브랜치에 통합할 때 주로 쓰인다.
재귀(recursive) 병합 전략은 공통 조상이 여러 개인 경우(크리스-크로스 병합 등)를 처리하기 위해 가상의 공통 조상을 먼저 병합해 사용하는 고급 알고리즘이다. Git 2.33 이후에는 ort 전략이 기본값으로 채택되어 더 빠르고 정확하다.
git merge -s ort feature # ort 전략 (Git 2.33+ 기본값) git merge -s recursive feature # recursive (구 기본값) git merge -s octopus b1 b2 b3 # 3개 브랜치 동시 병합 git merge -s ours feature # feature를 "병합했다는 기록만" 남기고 내용은 무시