Conflict 해결: 같은 줄을 서로 다르게 수정했을 때 Git이 판단을 사람에게 넘기는 방식
충돌(Conflict)은 Git이 두 브랜치의 변경을 자동으로 합칠 수 없을 때 발생한다. 충돌 마커의 구조와 3-way merge 원리를 이해하면 어떤 코드를 남길지 판단하는 기준이 생긴다. 이 글에서는 충돌을 직접 만들고 해결한 뒤 커밋하는 전 과정과, 중단·재시도 방법, 실무에서의 예방 습관까지 다룬다.
By 송민성
1. 개념
충돌(Conflict)은 병합(merge)이나 리베이스(rebase), 체리픽(cherry-pick) 중 Git이 두 변경 사항을 자동으로 하나로 합칠 수 없을 때 발생하는 상태다. 대표적인 경우는 두 브랜치가 같은 파일의 같은 영역을 서로 다르게 수정한 경우다. 한쪽이 파일을 삭제했는데 다른 쪽이 수정한 경우, 같은 이름의 파일을 양쪽에서 각각 새로 추가한 경우에도 충돌이 난다.
충돌은 오류가 아니라 "어느 쪽을 택할지 사람이 결정해 달라"는 요청이다. 해결이 끝나기 전까지 Git은 병합을 완료하지 않고 작업 트리(working tree)를 충돌 상태로 유지한다.
2. 왜 사용하는가
Git은 여러 사람이 동시에 같은 코드베이스를 수정하는 것을 전제로 설계되었다. 대부분의 변경은 서로 다른 영역이어서 자동으로 합쳐지지만, 의미를 판단해야 하는 경우까지 Git이 임의로 선택하면 한쪽의 작업이 조용히 사라질 수 있다. 충돌 메커니즘은 이런 데이터 손실을 막기 위해 판단을 사람에게 넘기는 안전장치다.
따라서 충돌 해결 능력은 협업의 기본 기술이다. 해결 방법을 모르면 충돌이 두려워 브랜치를 오래 방치하게 되고, 그럴수록 충돌은 더 커진다.
3. 동작 원리
3-way merge
Git은 병합할 때 두 브랜치의 끝 커밋만 비교하지 않는다. 두 브랜치의 공통 조상(merge base)을 찾아 세 지점을 비교한다.
- base: 공통 조상 커밋의 내용
- ours: 현재 브랜치(HEAD)의 내용
- theirs: 병합해 오는 브랜치의 내용
영역별 판단 규칙은 다음과 같다.
| base 대비 ours | base 대비 theirs | 결과 |
|---|---|---|
| 변경 없음 | 변경 있음 | theirs 채택 (자동) |
| 변경 있음 | 변경 없음 | ours 채택 (자동) |
| 동일하게 변경 | 동일하게 변경 | 그대로 채택 (자동) |
| 서로 다르게 변경 | 서로 다르게 변경 | 충돌 |
충돌 마커
충돌 영역은 파일 안에 다음 마커로 표시된다.
<<<<<<< HEAD
현재 브랜치(ours)의 내용
=======
병합해 오는 브랜치(theirs)의 내용
>>>>>>> feature/greeting<<<<<<<와 ======= 사이가 ours, =======와 >>>>>>> 사이가 theirs다. 해결이란 이 마커를 모두 지우고 최종적으로 남길 내용만 파일에 남기는 일이다. ours와 theirs 중 하나를 고를 수도 있고, 둘을 합치거나 완전히 새로 쓸 수도 있다.
4. 코드 예제
아래 스크립트는 빈 디렉터리에서 충돌을 직접 만들고 해결한다. 작업 디렉터리 밖의 임시 폴더에서 실행한다.
set -e
mkdir conflict-demo && cd conflict-demo
git init -b main
git config user.name "demo"
git config user.email "demo@example.com"
echo 'print("hello")' > app.py
git add app.py && git commit -m "init: app.py 추가"
# feature 브랜치에서 같은 줄을 수정
git switch -c feature/greeting
echo 'print("안녕하세요")' > app.py
git commit -am "feat: 한국어 인사"
# main에서도 같은 줄을 다르게 수정
git switch main
echo 'print("hello, world")' > app.py
git commit -am "feat: hello world"
# 병합 시도 -> 충돌 발생 (exit code 1이므로 || true)
git merge feature/greeting || true
git status
cat app.pygit status에는 both modified: app.py가 표시되고, app.py에는 충돌 마커가 들어 있다. 이어서 해결한다.
# 1) 파일을 열어 최종 내용으로 직접 편집 (여기서는 두 인사를 모두 남긴다)
cat > app.py <<'EOF'
print("hello, world")
print("안녕하세요")
EOF
# 2) 마커가 남아 있지 않은지 확인
grep -nE '^(<<<<<<<|=======|>>>>>>>)' app.py || echo "마커 없음"
# 3) 해결 완료를 Git에 알리고 병합 커밋 생성
git add app.py
git commit -m "merge: feature/greeting 병합, 인사말 둘 다 유지"
git log --oneline --graph한쪽을 통째로 택하는 경우에는 다음 명령을 쓴다.
# 충돌 중인 파일에 대해 현재 브랜치 버전 채택
git checkout --ours -- app.py
# 또는 병합해 오는 브랜치 버전 채택
git checkout --theirs -- app.py
git add app.py
git commit병합이 부담스러우면 언제든 되돌릴 수 있다.
git merge --abort # 병합 시작 이전 상태로 복귀
git rebase --abort # 리베이스 중이라면5. 시간 복잡도 또는 성능 특성
충돌 해결은 알고리즘 복잡도보다 사람의 비용이 지배적이다. 다음 특성은 일반적으로 성립한다.
- 충돌 가능성은 두 브랜치가 갈라진 뒤 서로 수정한 영역이 겹칠수록 커진다. 브랜치 수명이 길고 변경 범위가 넓을수록 해결 비용이 커진다.
- 리베이스는 커밋을 하나씩 다시 적용하므로, 같은 영역을 건드린 커밋이 여러 개면 같은 지점에서 충돌을 여러 번 해결해야 할 수 있다. 병합은 최종 상태를 한 번에 합치므로 충돌 해결이 한 번으로 끝난다.
6. 실무 사용 사례
- Pull Request 병합 전 동기화: 기능 브랜치에
git merge main또는git rebase main을 먼저 수행해 로컬에서 충돌을 해결한 뒤 푸시한다. - 의존성 잠금 파일:
package-lock.json같은 생성 파일은 직접 편집하지 말고, 한쪽 버전을 택한 뒤 패키지 매니저로 재생성하는 방식이 일반적이다. - IDE 병합 도구: VS Code 등은 base/ours/theirs를 나란히 보여 주는 3-way 편집기를 제공한다.
git mergetool로 외부 도구를 연결할 수도 있다. - 재발 충돌 기록: 같은 충돌을 반복 해결해야 한다면
git config rerere.enabled true로 해결 방식을 기록하고 자동 재적용하게 할 수 있다.
7. 주의할 점
- 마커를 남긴 채 커밋하지 않는다.
<<<<<<<등이 코드에 그대로 들어가면 문법 오류나 오동작으로 이어진다. 커밋 전에grep으로 확인하는 습관을 들인다. - 해결 후 반드시 빌드와 테스트를 돌린다. 텍스트상 충돌이 없어도 의미상 충돌(예: 한쪽이 함수 시그니처를 바꾸고 다른 쪽이 옛 시그니처로 호출)은 Git이 감지하지 못한다.
- `--ours`와 `--theirs`는 리베이스에서 의미가 뒤집힌다. 리베이스 중에는 ours가 "기준이 되는 대상 브랜치", theirs가 "지금 다시 적용 중인 내 커밋"을 가리킨다.
- 무조건 한쪽을 택하지 않는다. 상대방의 변경 의도를 모르겠다면 해당 작성자에게 확인한다.
- 이미 공유한 브랜치를 리베이스하지 않는다. 히스토리가 바뀌어 다른 사람의 충돌을 키운다.
8. 핵심 정리
- 충돌은 base 대비 ours와 theirs가 같은 영역을 서로 다르게 바꿨을 때 발생한다.
- 해결 절차는 마커 확인 → 최종 내용으로 편집 → 마커 제거 확인 →
git add→git commit(리베이스는git rebase --continue)이다. - 막히면
git merge --abort,git rebase --abort로 시작 전 상태로 돌아갈 수 있다. - 충돌이 없어도 의미상 오류는 남을 수 있으므로 해결 후 테스트가 필요하다.
- 브랜치를 짧게 유지하고 자주 main과 동기화하면 충돌 규모를 줄일 수 있다.