Git & GitHub

Merge Conflict는 발생하는 이유가 뭘까?

승주우에요 2026. 4. 9. 06:54

1. Merge Conflict란?

Git은 두 브랜치를 병합할 때 변경 사항을 자동으로 합친다.
그러나 같은 파일의 같은 라인을 두 브랜치가 서로 다르게 수정했다면, Git은 어느 쪽이 맞는지 판단할 수 없다. 이 상태가 바로 Merge Conflict이다.

참고 — Git이 자동으로 합칠 수 있는 경우(서로 다른 파일 수정, 같은 파일의 다른 라인 수정)는 컨플릿 없이 머지됩니다. 컨플릿은 정말로 충돌이 일어났을 때만 발생한다.


2. 발생 원인

Merge Conflict의 근본 원인은 두 브랜치가 서로의 변경 사항을 모르는 채로 같은 라인을 수정했을 때다. 반대로, 한 브랜치가 다른 브랜치의 커밋 이력을 포함하고 있다면 Git은 충돌 없이 머지할 수 있다.

시나리오 1 — 동일 파일, 동일 라인 수정

가장 흔한 케이스입니다. 두 브랜치가 같은 함수나 변수를 각각 다르게 수정했을 때 발생합니다.

main        ──── A ──────────────────── M
                  \                    /
feature-a          ── B1 ── B2  (login.py 3번 라인 수정)
                  \
feature-b          ── C1 ── C2  (login.py 3번 라인 수정)
                                       ↑ 충돌!

Git이 컨플릿을 판단하는 방식 — 3-way merge

feature-b가 머지될 때 Git은 단순히 두 브랜치만 비교하는 것이 아니라, 분기 시점의 공통 조상(base) 까지 포함해 세 가지를 비교한다.

① base      → "Hello"          (분기 시점 원본)
② develop   → "Hello, Korea"   (feature-a가 이미 머지된 상태)
③ feature-b → "Hello, Japan"   (base에서 독립적으로 수정)

Git 입장에서는 develop과 feature-b가 모두 같은 라인을 base 기준으로 다르게 수정했기 때문에 어느 쪽이 맞는지 판단할 수 없어 컨플릿을 발생시킨다.

feature-b는 develop을 당겨오지 않은 채 작업했기 때문에 feature-a의 변경을 전혀 모르는 상태 이것이 충돌의 근본 원인이다

develop:   A ── feature-a 머지 ──→ "Korea"
               ↑
           여기서 분기
               ↓
feature-b: 독립적으로 작업 ──────→ "Japan"  ← develop 변경을 모름, 항상 develop을 pull해주고 merge 해줘야 함

시나리오 2 — 베이스 브랜치가 많이 앞서간 경우

feature 브랜치를 오래 방치한 뒤 머지를 시도하면, 그 사이 develop에 쌓인 변경 사항과 충돌할 가능성이 높아집니다.

시나리오 3 — 파일 삭제 vs 수정

한쪽 브랜치에서 파일을 삭제하고, 다른 브랜치에서 같은 파일을 수정하면 Git이 처리 방법을 결정할 수 없어 컨플릿이 발생합니다.

 

-> 결국 시나리오 1,2,3의 근본적인 원인은 develop pull해주고 변경사항을 feature 브랜치에 반영을 해줘야 한다는 점이다.


3. 컨플릿 마커 읽는 법

Merge Conflict가 발생하면 Git은 해당 파일에 컨플릿 마커를 삽입합니다.

def get_greeting():
<<<<<<< HEAD
    return "Hello, Korea"   # 현재 브랜치 (내 변경)
=======
    return "Hello, Japan"   # 머지하려는 브랜치 (상대방 변경)
>>>>>>> feature/greeting

마커 의미

<<<<<<< HEAD 현재 체크아웃된 브랜치의 변경 내용 시작
======= 두 변경 사항의 구분선
>>>>>>> branch-name 머지하려는 브랜치의 변경 내용 끝

이 마커 사이의 내용을 직접 편집해 원하는 최종 코드로 만들어야 합니다.


4. CLI로 해결하기

4-1. 컨플릿 발생 확인

$ git merge feature/greeting

Auto-merging greeting.py
CONFLICT (content): Merge conflict in greeting.py
Automatic merge failed; fix conflicts and then commit the result.

4-2. 충돌 파일 확인

$ git status

Unmerged paths:
  (use "git add <file>..." to mark resolution)
        both modified:   greeting.py

4-3. 파일 직접 편집

에디터로 파일을 열어 마커를 제거하고 최종 코드로 정리합니다.

# 수정 전 (컨플릿 마커 포함)
<<<<<<< HEAD
    return "Hello, Korea"
=======
    return "Hello, Japan"
>>>>>>> feature/greeting

# 수정 후 (원하는 코드로 정리)
    return "Hello, Korea"   # 최종 결정

4-4. 해결 완료 후 커밋

# 해결된 파일을 스테이징
$ git add greeting.py

# 머지 커밋 생성
$ git commit -m "resolve: greeting.py merge conflict"

주의 — git add 전에 파일 내에 <<<<<<<, =======, >>>>>>> 마커가 남아있지 않은지 반드시 확인해야함!


5. GitHub UI로 해결하기

CLI가 불편하다면 GitHub에서 제공하는 웹 에디터로도 해결할 수 있습니다.

순서

  1. PR 페이지에서 Resolve conflicts 버튼 클릭
  2. 웹 에디터에서 컨플릿 마커 확인
  3. 원하는 코드만 남기고 마커 삭제
  4. 우측 상단 Mark as resolved 클릭
  5. Commit merge 버튼으로 커밋

한계 — GitHub UI는 단순한 텍스트 충돌만 처리할 수 있다. 복잡한 충돌이나 바이너리 파일 충돌은 로컬에서 직접 해결해야 한다.


6. 예방 팁

Merge Conflict를 완전히 피할 수는 없지만, 빈도를 크게 줄일 수 있습니다.

자주 동기화하기

feature 브랜치를 오래 방치할수록 베이스 브랜치와의 격차가 커집니다. 작업 중에도 주기적으로 베이스 브랜치를 당겨오는 습관을 들이세요.

# 작업 중 develop 최신 상태 반영
$ git fetch origin
$ git rebase origin/develop

브랜치 수명을 짧게 유지하기

브랜치가 오래 살수록 충돌 가능성이 높아집니다. 작은 단위로 나눠 PR을 자주 올리는 것이 효과적이다.

파일/모듈 담당 영역 나누기

팀원 간에 담당 파일이나 모듈을 명확히 나누면 같은 파일을 동시에 수정하는 상황 자체를 줄일 수 있다.

PR 크기를 작게 유지하기

한 번에 많은 변경을 담은 PR은 충돌 가능성도 높고 리뷰도 어렵습니다. 기능 단위로 잘게 나눠 PR을 올리는 것을 권장


7. pull vs rebase vs merge — 언제 무엇을 쓰나

브랜치 동기화 방법으로 pull, rebase, merge 세 가지가 자주 언급됩니다. 각각 용도가 다릅니다.

pull — local develop 최신화

로컬 develop 브랜치를 원격과 맞출 때 사용합니다. develop은 직접 커밋하지 않으므로 단순히 당겨오면 충분합니다.

# local develop에서
git pull origin develop

rebase — feature 브랜치에 develop 반영

feature 브랜치에서 develop의 최신 변경을 반영할 때 사용하는데, 커밋 히스토리가 선형으로 유지되어 git log가 깔끔해진다.

# feature 브랜치에서
git fetch origin
git rebase origin/develop

rebase는 "내 커밋들을 develop의 최신 커밋 위로 옮기는" 동작이다. (아, 나는 반대로 알고 있었다 ㅜ)

# rebase 전
develop:   A --- B --- C
                 \
feature:          D --- E

# rebase 후
develop:   A --- B --- C
                         \
feature:                  D' --- E'  ← C 위로 이동

merge — 공식 브랜치 합치기

develop → main처럼 공식 브랜치를 합치거나, PR 머지처럼 기능 단위 이력을 보존해야 할 때 사용합니다. 머지 커밋이 생기면서 두 히스토리가 합쳐진 기록이 남습니다.

# main 브랜치에서
git merge develop
# merge 결과
develop:   A --- B --- C
                 \         \
main:             D --- E ---M  ← 머지 커밋

정리

상황 명령 이유

local develop 최신화 git pull origin develop 단순 동기화
feature에 develop 반영 git rebase origin/develop 히스토리 선형 유지
develop → main 반영 git merge develop 기능 단위 이력 보존
PR 머지 (GitHub) merge 브랜치 합류 기록 남기기

개인 작업 중 동기화 → rebase, 공식 브랜치 합치기 → merge 로 기억하면 편합니다.


마무리

Merge Conflict는 협업 과정에서 피할 수 없는 상황이지만, 발생 원리를 이해하고 있다면 당황하지 않고 침착하게 해결할 수 있다. 이 또한, 소통이 매우 중요하리 ....