본문 바로가기
TYLER SONGBlog
← Blog index
CS학습5 min read

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)을 사용한다.

text
        A---B---C   feature
       /
  o---o---D---E     main   (HEAD)

여기서 merge base는 o(D 직전의 커밋)다. Git은 다음 세 스냅샷을 비교한다.

  1. merge base
  2. 현재 브랜치 끝 (E)
  3. 병합 대상 브랜치 끝 (C)

각 파일의 각 영역에 대해 다음 규칙이 적용된다.

  • 한쪽만 base에서 바뀌었다면 그 변경을 채택한다.
  • 양쪽이 같은 방식으로 바뀌었다면 그대로 채택한다.
  • 양쪽이 같은 영역을 서로 다르게 바꿨다면 충돌(conflict) 이다.

충돌이 없으면 Git은 결과를 담은 병합 커밋 M을 만든다. M의 부모는 E와 C 둘이다.

text
        A---B---C
       /         \
  o---o---D---E---M   main

fast-forward

main에서 갈라진 뒤 main에 새 커밋이 없다면, feature의 끝 커밋은 main의 직계 후손이다. 이때는 합칠 내용이 없으므로 main 포인터를 feature의 끝으로 옮기기만 한다.

충돌 표시

충돌한 파일에는 Git이 다음과 같은 마커를 삽입한다.

text
<<<<<<< HEAD
현재 브랜치의 내용
=======
병합 대상 브랜치의 내용
>>>>>>> feature

개발자가 이 부분을 직접 편집해 마커를 제거하고, git add로 해결을 표시한 뒤 커밋하면 병합이 완료된다.

4. 코드 예제

아래 스크립트는 임시 디렉터리에 저장소를 만들어 fast-forward, 3-way 병합, 충돌 해결을 차례로 재현한다.

shell
#!/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가 가능한 상황에서도 병합 커밋을 강제로 만든다.

shell
git merge --no-ff feature-a

5. 시간 복잡도 또는 성능 특성

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로 취소할 수 있다.
  • 병합은 기존 커밋을 바꾸지 않아 공유 브랜치에서도 안전하지만, 병합 후에는 반드시 테스트로 의미상 오류를 확인한다.