git merge: 브랜치를 합치고 충돌을 해결하는 방법
git merge는 다른 브랜치의 히스토리를 현재 브랜치에 합칩니다. 현재 브랜치가 앞서 나가지 않았다면 Git은 포인터만 옮기는 fast-forward를 하고, 그렇지 않으면 부모가 둘인 병합 커밋을 만듭니다. 대상 브랜치로 이동해 git merge 브랜치이름을 실행하고, 충돌이 나면 파일을 고친 뒤 git add와 git commit을 하거나 git merge --abort로 전부 취소합니다.
git merge가 하는 일
git merge는 지정한 브랜치의 변경을 지금 있는 브랜치에 넣습니다. 방향을 기억하세요. 병합은 "자기 자신으로" 합칩니다. feature/login을 main에 합치려면 먼저 main으로 이동한 뒤 병합을 실행합니다.
git switch main
git pull # 서버에서 최신 main 받기
git merge feature/login
Git은 코드를 한 줄씩 손으로 복사하지 않습니다. 두 브랜치의 공통 조상(merge base)을 찾고, 그 조상과 두 버전을 비교한 뒤 변경을 합칩니다. 양쪽이 서로 다른 곳을 고쳤다면 결과는 자동으로 나옵니다. 같은 곳을 고쳤다면 충돌이 생기고, 해결은 여러분이 합니다.
두 가지 방식: fast-forward와 3-way 병합
Fast-forward: 병합 커밋이 필요 없을 때
main에서 feature를 떼어 내고 커밋을 세 개 한 뒤, 그동안 main에 아무도 아무것도 추가하지 않았다고 해 봅시다. 그러면 main의 히스토리는 feature 히스토리의 시작 부분일 뿐입니다. Git은 main 포인터를 feature의 마지막 커밋으로 앞으로 옮기고, 새 커밋은 만들지 않습니다.
$ git merge feature
Updating 3f2a1c4..9b7e0d2
Fast-forward
src/auth.js | 42 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 42 insertions(+)
히스토리는 일직선으로 유지되지만, "여기서 feature 브랜치가 있었다"는 사실은 사라집니다. 이것이 좋을 수도 나쁠 수도 있으며, 아래 --no-ff 절에서 설명합니다.
3-way 병합과 병합 커밋
두 브랜치가 모두 공통 조상에서 앞으로 나아갔다면 포인터를 옮길 수 없습니다. Git은 3-way 병합을 합니다. 기준(조상), "우리" 쪽(현재 브랜치), "그들" 쪽(병합하는 브랜치)을 비교합니다. 결과는 부모가 둘인 별도의 병합 커밋으로 기록됩니다.
$ git merge feature
Merge made by the 'ort' strategy.
src/auth.js | 42 ++++++++++++++++++
src/router.js | 3 +-
2 files changed, 44 insertions(+), 1 deletion(-)
Git은 Merge branch 'feature' 같은 메시지로 에디터를 엽니다. 그대로 두거나, 왜 병합했는지 덧붙일 수 있습니다. 결과 "갈림길"을 보려면 다음을 사용하세요.
git log --oneline --graph --all
Git 2.34부터 기본 전략은 ort이며, 이전에는 recursive였습니다. 일상 작업에서는 전략을 바꿀 필요가 없습니다.
git merge의 유용한 옵션
| 옵션 | 동작 | 사용하는 경우 |
|---|---|---|
--no-ff | fast-forward가 가능해도 항상 병합 커밋을 만듦 | 같은 기능에서 온 커밋들이 히스토리에서 묶여 보이길 원할 때 |
--ff-only | fast-forward로만 병합하고, 아니면 오류로 중단 | 불필요한 병합 커밋 없이 로컬 브랜치를 갱신할 때 |
--squash | 브랜치의 모든 변경을 인덱스에 넣지만 커밋하지 않고 브랜치와의 연결도 남기지 않음 | 기능 하나를 깔끔한 커밋 하나로 만들고 싶을 때 |
--no-commit | 병합은 수행하되 커밋 직전에 멈춤 | 커밋 전에 결과를 확인하거나 테스트를 돌리고 싶을 때 |
-m "텍스트" | 병합 커밋의 메시지 지정 | 에디터 없이 스크립트에서 병합할 때 |
--abort | 진행 중인 병합을 취소하고 그 이전 상태로 복구 | 충돌이 너무 많아서 처음부터 다시 하고 싶을 때 |
--continue | 충돌을 해결한 뒤 병합을 마무리 | 마지막 단계에서 git commit 대신 쓸 때 |
-X ours / -X theirs | 충돌하는 조각에서 우리 또는 그들의 버전을 자동 선택 | 어느 쪽이 맞는지 확실히 알 때(주의해서 사용) |
--no-ff: 기능의 경계 유지하기
많은 팀이 기능을 --no-ff로만 합칩니다. 그러면 그래프에서 작업마다 하나의 "거품"으로 보이고, git revert -m 1 <병합 커밋> 한 번으로 되돌릴 수 있습니다. fast-forward에서는 그런 지점이 없어서, 되돌리려면 커밋을 하나씩 되돌려야 합니다.
--squash: 열 개 커밋 대신 하나
git switch main
git merge --squash feature/login
git commit -m "이메일과 비밀번호로 로그인"
--squash 후 Git은 브랜치를 병합된 것으로 보지 않습니다. git branch -d feature/login은 "not fully merged" 경고와 함께 삭제를 거부합니다. 모든 것이 제자리에 있다고 확신할 때 git branch -D로 삭제하세요. 또한 squash 후에 같은 브랜치에서 계속 작업하지 마세요. 다음 병합에서 이미 합쳐진 변경이 다시 들어와서 거의 확실히 충돌이 납니다.
병합 충돌과 해결 방법
충돌은 사고가 아니라 정상적인 작업 상황입니다. 두 브랜치가 같은 줄을 바꿨거나, 한쪽이 다른 쪽이 고친 파일을 삭제했다는 뜻입니다. Git은 어느 버전이 맞는지 추측하지 않습니다.
$ git merge feature/login
Auto-merging src/config.js
CONFLICT (content): Merge conflict in src/config.js
Automatic merge failed; fix conflicts and then commit the result.
1단계. 무엇이 충돌하는지 보기
$ git status
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: src/config.js
2단계. 파일 고치기
충돌 파일에는 Git이 마커를 넣습니다.
<<<<<<< HEAD
const TIMEOUT = 5000;
=======
const TIMEOUT = 10000;
>>>>>>> feature/login
위쪽은 현재 브랜치(HEAD), 아래쪽은 병합하는 브랜치입니다. 맞는 쪽을 남기거나(둘을 합쳐도 됩니다), 마커 세 줄을 모두 지우고 파일을 저장하세요. 자주 하는 실수는 파일 중간의 마커를 놓치는 것입니다. 커밋 전에 git diff --check가 남은 마커를 표시해 줍니다.
3단계. 해결을 표시하고 병합 마무리하기
git add src/config.js
git commit # 또는 git merge --continue
충돌 전체를 한쪽 선택으로 해결할 수 있다면 파일을 직접 고칠 필요가 없습니다.
git checkout --ours src/config.js # 현재 브랜치 버전 유지
git checkout --theirs src/config.js # 병합하는 브랜치 버전 가져오기
git add src/config.js
편집기에서 하는 편이 편합니다
VS Code, JetBrains IDE 등은 충돌을 세 칸으로 보여 주고 "현재 것 수락 / 들어오는 것 수락 / 둘 다 수락" 버튼을 제공합니다. git mergetool로 외부 도구를 연결할 수도 있습니다. 유용한 설정은 git config --global merge.conflictStyle zdiff3입니다. 마커에 공통 조상의 원래 버전도 나타나서, 누가 무엇을 바꿨는지 알기 쉬워집니다.
마음이 바뀌었다면 병합 취소하기
git merge --abort
이 명령은 작업 폴더와 인덱스를 git merge 이전 상태로 되돌립니다. 그래서 병합 전에 작업 트리를 깨끗하게 두는 것이 좋습니다. 커밋하거나 git stash로 미완성 수정을 치워 두세요. 그렇지 않으면 병합 결과와 섞여서 구분하기 어려워집니다.
이미 끝난 병합 되돌리기
병합 커밋이 아직 서버에 올라가지 않았다면 브랜치를 뒤로 되돌리는 것이 가장 간단합니다.
git reset --hard ORIG_HEAD
ORIG_HEAD는 병합 같은 위험한 작업 직전에 Git이 기록해 두는 위치로, 병합 이전의 브랜치 위치입니다. --hard는 커밋하지 않은 변경을 모두 버리므로 git status를 먼저 확인하세요.
병합이 이미 공유 저장소에 있다면 히스토리를 다시 쓰지 않습니다. 대신 반대 방향의 커밋을 만듭니다.
git revert -m 1 a1b2c3d
-m 1은 "주" 부모가 첫 번째 부모(병합된 쪽 브랜치)라는 뜻입니다. 주의할 점: 이렇게 revert하면 Git은 기능의 커밋이 이미 합쳐졌다고 봅니다. 나중에 고쳐진 기능을 다시 합치려면, 먼저 revert 커밋 자체를 되돌려야 합니다.
팀에서 병합하기
git pull은 fetch와 merge의 조합
일반 git pull은 변경을 내려받고 원격 브랜치를 로컬 브랜치와 병합합니다. 동료와 같은 시점에 커밋했다면 "Merge branch 'main' of ..." 커밋이 생깁니다. 이런 기술적인 병합을 피하려면 git config --global pull.ff only를 설정하세요(fast-forward가 불가능하면 pull이 실패하고, 어떻게 할지는 여러분이 정합니다). 또는 git pull --rebase를 쓰세요.
Pull request와 Merge 버튼
GitHub와 GitLab에서는 보통 pull/merge request를 통해 병합합니다. "Create a merge commit" 버튼은 git merge --no-ff와 같고, "Squash and merge"는 압축 병합, "Rebase and merge"는 rebase 병합입니다. 어떤 방식을 쓸지는 팀의 합의이며, 메커니즘은 콘솔과 같습니다.
main을 내 브랜치로 가져오기
오래 사는 브랜치는 main에서 점점 멀어지고, 충돌이 쌓입니다. 주 브랜치를 자주 가져오세요.
git switch feature/login
git fetch origin
git merge origin/main
이렇게 하면 충돌이 생길 때마다 작은 단위로 해결되고, 마지막 main으로의 병합은 매끄럽게 끝납니다.
흔한 실수
- 반대 방향으로 병합.
main에 있으면서git merge main을 하거나, 기능을 다른 기능에 합치는 실수. 병합 전에git branch --show-current로 현재 브랜치를 확인하세요. - 오래된 브랜치와 병합. 일주일 동안 갱신하지 않은 로컬
main과 병합하는 실수. 먼저git pull을 하고, 그다음 병합하세요. - "Your local changes would be overwritten by merge". 같은 파일에 커밋하지 않은 수정이 있습니다. 커밋하거나 stash로 치워 두세요.
- "refusing to merge unrelated histories". 두 브랜치에 공통 조상이 없습니다. 예를 들어 로컬 저장소는
git init으로 만들었고, 원격에는 이미 README가 있는 경우입니다. 정말 합쳐야 한다면--allow-unrelated-histories를 추가하세요. - 충돌 마커를 커밋함. 빌드가
<<<<<<<에서 깨집니다. 커밋 전에git diff --check를 실행하고 테스트를 돌리세요.
정리
git merge는 안전하게 브랜치를 합치는 방법입니다. 기존 커밋을 다시 쓰지 않고, 새 커밋을 추가하거나 포인터를 옮길 뿐입니다. 합칠 브랜치로 이동하고, 작업 트리를 깨끗하게 유지하고, 기능의 경계가 중요하다면 --no-ff를, 커밋 하나가 필요하다면 --squash를 쓰세요. 충돌은 파일 수정, git add, git commit의 세 단계로 해결하며, git merge --abort는 언제나 처음 상태로 되돌려 줍니다. 브랜치 병합과 충돌 해결은 Git 강의에서 실습으로 다룹니다.
자주 묻는 질문
git pull과 git merge는 무엇이 다른가요?
git pull은 먼저 서버에서 내려받고(git fetch), 그다음 원격 브랜치를 현재 브랜치와 합칩니다. 설정에 따라 merge 대신 rebase를 할 수도 있습니다. git merge는 로컬에 이미 있는 것만 다룹니다.
여러 브랜치를 한 번에 합칠 수 있나요?
네, git merge 기능A 기능B를 실행하면 부모가 여러 개인 옥토퍼스 병합을 할 수 있지만, 충돌이 없을 때만 됩니다. 실무에서는 보통 브랜치를 하나씩 합칩니다.
현재 브랜치에 이미 합쳐진 브랜치는 어떻게 확인하나요?
git branch --merged는 현재 브랜치에 완전히 합쳐진 브랜치를, git branch --no-merged는 아직 합쳐지지 않은 브랜치를 보여 줍니다. 합쳐진 브랜치는 git branch -d로 안전하게 지울 수 있습니다.
병합 결과를 미리 확인할 방법이 있나요?
git log main..feature로 main에 없는 커밋을, git diff main...feature로 공통 조상 이후 브랜치의 변경을 볼 수 있습니다.
merge 후 ORIG_HEAD는 무엇인가요?
병합 직전의 위치를 가리키는 참조입니다. 서버에 올리기 전이라면 git reset --hard ORIG_HEAD로 로컬 병합을 되돌릴 수 있습니다.