Merge: 갈라진 브랜치의 작업 이력을 하나로 합치는 Git 연산
Git의 merge는 서로 다른 브랜치에서 진행된 작업을 하나의 브랜치로 통합하는 명령이다. 공통 조상(merge base)과 두 브랜치의 끝 커밋을 비교하는 3-way merge로 동작하며, 상황에 따라 fast-forward 또는 병합 커밋(merge commit)이 만들어진다. 충돌(conflict)이 발생하면 개발자가 직접 해결해야 한다.
By 송민성
1. 개념
병합(merge)은 한 브랜치의 변경 이력을 현재 브랜치에 통합하는 Git 연산이다. 현재 체크아웃한 브랜치(HEAD가 가리키는 브랜치)에 대상 브랜치의 커밋들을 합치며, 대상 브랜치 자체는 변하지 않는다.
병합 결과는 크게 두 가지 형태다.
- 빨리 감기 병합(fast-forward merge): 현재 브랜치가 대상 브랜치의 조상일 때, 브랜치 포인터만 앞으로 이동한다. 새 커밋은 생기지 않는다.
- 3-way 병합(three-way merge): 두 브랜치가 갈라진 상태일 때, 두 부모를 가진 병합 커밋(merge commit)을 새로 만든다.
2. 왜 사용하는가
- 기능 개발, 버그 수정을 브랜치로 분리해 병렬로 작업한 뒤 결과를 하나로 모을 수 있다.
- 병합 커밋은 두 부모를 가지므로 "어느 브랜치가 언제 합쳐졌는지"가 이력에 그대로 남는다.
- 기존 커밋을 수정하지 않으므로, 이미 공유한 브랜치에서도 안전하게 쓸 수 있다. (리베이스와 달리 커밋 해시가 바뀌지 않는다.)
3. 동작 원리
3-way merge
Git은 두 브랜치의 끝 커밋과 그 둘의 가장 가까운 공통 조상인 병합 기준점(merge base)을 사용한다.
A---B---C feature
/
o---o---D---E main (HEAD)여기서 merge base는 o(D 직전의 커밋)다. Git은 다음 세 스냅샷을 비교한다.
- merge base
- 현재 브랜치 끝 (E)
- 병합 대상 브랜치 끝 (C)
각 파일의 각 영역에 대해 다음 규칙이 적용된다.
- 한쪽만 base에서 바뀌었다면 그 변경을 채택한다.
- 양쪽이 같은 방식으로 바뀌었다면 그대로 채택한다.
- 양쪽이 같은 영역을 서로 다르게 바꿨다면 충돌(conflict) 이다.
충돌이 없으면 Git은 결과를 담은 병합 커밋 M을 만든다. M의 부모는 E와 C 둘이다.
A---B---C
/ \
o---o---D---E---M mainfast-forward
main에서 갈라진 뒤 main에 새 커밋이 없다면, feature의 끝 커밋은 main의 직계 후손이다. 이때는 합칠 내용이 없으므로 main 포인터를 feature의 끝으로 옮기기만 한다.
충돌 표시
충돌한 파일에는 Git이 다음과 같은 마커를 삽입한다.
<<<<<<< HEAD
현재 브랜치의 내용
=======
병합 대상 브랜치의 내용
>>>>>>> feature개발자가 이 부분을 직접 편집해 마커를 제거하고, git add로 해결을 표시한 뒤 커밋하면 병합이 완료된다.
4. 코드 예제
아래 스크립트는 임시 디렉터리에 저장소를 만들어 fast-forward, 3-way 병합, 충돌 해결을 차례로 재현한다.
#!/bin/zsh
set -e
dir=$(mktemp -d)
cd "$dir"
git init -q -b main
git config user.name "Demo"
git config user.email "demo@example.com"
# 초기 커밋
printf 'line1\nline2\nline3\n' > app.txt
git add app.txt && git commit -qm "init"
# 1) fast-forward 병합
git switch -qc feature-a
echo "feature-a" > a.txt
git add a.txt && git commit -qm "add a.txt"
git switch -q main
git merge feature-a # Fast-forward
git log --oneline --graph
# 2) 3-way 병합 (서로 다른 파일 수정)
git switch -qc feature-b
echo "feature-b" > b.txt
git add b.txt && git commit -qm "add b.txt"
git switch -q main
echo "main-change" > c.txt
git add c.txt && git commit -qm "add c.txt on main"
git merge --no-edit feature-b # 병합 커밋 생성
git log --oneline --graph
# 3) 충돌 발생과 해결
git switch -qc feature-c
sed -i.bak 's/line2/line2-from-feature/' app.txt && rm app.txt.bak
git commit -qam "edit line2 on feature-c"
git switch -q main
sed -i.bak 's/line2/line2-from-main/' app.txt && rm app.txt.bak
git commit -qam "edit line2 on main"
git merge feature-c || true # CONFLICT 발생
git status --short # UU app.txt
cat app.txt # 충돌 마커 확인
# 수동 해결: 두 변경을 합친 내용으로 덮어쓰기
printf 'line1\nline2-merged\nline3\n' > app.txt
git add app.txt
git commit -qm "Merge feature-c: resolve conflict"
git log --oneline --graph
# 병합 취소가 필요할 때는 병합 중에 아래 명령을 사용한다.
# git merge --abort--no-ff 옵션을 쓰면 fast-forward가 가능한 상황에서도 병합 커밋을 강제로 만든다.
git merge --no-ff feature-a5. 시간 복잡도 또는 성능 특성
Git은 병합 비용을 big-O로 공식 정의하지 않는다. 다만 다음 특성은 확실하다.
- fast-forward는 브랜치 포인터(참조)만 갱신하므로 저장소 크기와 무관하게 매우 가볍다.
- 3-way 병합은 merge base를 찾는 과정과, 양쪽에서 변경된 파일을 비교하는 과정이 필요하다. 비용은 대체로 두 브랜치 사이에서 실제로 달라진 파일과 변경량에 비례한다. 전체 프로젝트 크기에 비례하지 않는다.
- 병합은 기존 커밋 객체를 다시 쓰지 않고 병합 커밋 하나를 추가할 뿐이다.
6. 실무 사용 사례
- Pull Request 병합: GitHub 등에서 "Create a merge commit" 방식은
--no-ff병합과 같은 구조의 이력을 남긴다. 이 외에 squash, rebase 방식 옵션도 제공된다. - `git pull`: 기본 동작은
git fetch후 원격 추적 브랜치를 현재 브랜치에 병합하는 것이다. (설정에 따라 rebase로 바꿀 수 있다.) - 릴리스·핫픽스 브랜치 반영: 핫픽스를 main에 병합한 뒤 develop 등 다른 장기 브랜치에도 병합해 수정이 누락되지 않게 한다.
- main 최신 내용 가져오기: 작업 브랜치에서
git merge main으로 최신 변경을 반영해 충돌을 미리 해결한다.
7. 주의할 점
- 병합 전 작업 트리 정리: 커밋하지 않은 변경이 있으면 병합이 거부되거나 충돌 해결이 복잡해진다. 먼저 커밋하거나
git stash를 사용한다. - 병합 방향 확인:
git merge X는 "X를 현재 브랜치로" 합친다. 먼저git switch로 받는 쪽 브랜치로 이동해야 한다. - 충돌 마커 잔존:
<<<<<<<,=======,>>>>>>>를 지우지 않고 커밋하면 코드가 깨진다. 커밋 전에git diff --check나 빌드·테스트로 확인한다. - 충돌 없는 병합도 논리 오류 가능: Git은 텍스트 기준으로 합칠 뿐, 의미상 호환성은 보장하지 않는다. 병합 후 테스트를 실행한다.
- 병합 중단: 해결이 어렵다면
git merge --abort로 병합 시작 이전 상태로 돌아갈 수 있다. - 이력 복잡도: 병합 커밋이 많아지면
git log --graph가 복잡해진다. 팀의 브랜치 전략(병합/스쿼시/리베이스)을 미리 합의한다.
8. 핵심 정리
- merge는 대상 브랜치의 변경을 현재 브랜치에 통합하며, 대상 브랜치는 변하지 않는다.
- 현재 브랜치가 대상의 조상이면 fast-forward, 갈라졌으면 부모가 둘인 병합 커밋이 생긴다.
- 3-way 병합은 merge base와 두 브랜치 끝 커밋을 비교하고, 같은 영역을 다르게 수정한 경우 충돌이 된다.
- 충돌은 마커를 편집해 해결한 뒤
git add→git commit으로 마무리하며,git merge --abort로 취소할 수 있다. - 병합은 기존 커밋을 바꾸지 않아 공유 브랜치에서도 안전하지만, 병합 후에는 반드시 테스트로 의미상 오류를 확인한다.