콘텐츠로 이동

9장: Merge 완전 정복 — 병합과 충돌 해결

Merge란?

두 브랜치의 작업을 하나로 합치는 작업입니다.

main:     A → B → C ──── → M (병합 커밋)
               ↘        ↗
feature:        D → E

Merge의 세 가지 방식

1. Fast-Forward Merge

main에 새 커밋이 없고, feature만 앞에 있을 때 발생합니다.

병합 전:
  main:     A → B
  feature:  A → B → C → D

병합 후 (그냥 포인터가 앞으로 이동):
  main:     A → B → C → D
git checkout main
git merge feature

# 출력:
# Fast-forward
#  file.txt | 2 ++
#  1 file changed, 2 insertions(+)

특징: 병합 커밋이 생성되지 않습니다. 히스토리가 일직선으로 유지됩니다.

2. 3-Way Merge

main과 feature 양쪽 모두 새 커밋이 있을 때 발생합니다.

병합 전:
  main:     A → B → C
  feature:  A → B → D → E

병합 후:
  main:     A → B → C → M (병합 커밋)
                     ↗
  feature:  D → E
git checkout main
git merge feature

# 출력:
# Merge made by the 'ort' strategy.
#  file.txt | 5 ++++-
#  1 file changed, 4 insertions(+), 1 deletion(-)

3. Squash Merge

feature의 여러 커밋을 하나로 뭉쳐서 main에 합칩니다.

병합 전:
  main:     A → B
  feature:  A → B → C → D → E (작업 커밋들이 많음)

병합 후 (하나의 커밋으로 합쳐짐):
  main:     A → B → S (스쿼시 커밋, C+D+E 내용)
git checkout main
git merge --squash feature
git commit -m "feat: 검색 기능 추가"

실무에서 많이 씁니다. 작업 중간의 "오타 수정", "다시 수정" 같은 잡다한 커밋을 깔끔하게 하나로 정리할 수 있어요.

방식 비교

방식 히스토리 병합 커밋 용도
Fast-forward 일직선 없음 main이 안 변했을 때
3-way merge 분기+합류 있음 일반적인 병합
Squash 일직선 새 커밋 잡다한 커밋 정리

Non-Fast-Forward 강제하기

Fast-forward가 가능해도 항상 병합 커밋을 만들고 싶다면:

git merge --no-ff feature

브랜치가 존재했다는 히스토리를 남기고 싶을 때 유용합니다.

Rebase로 병합하기

merge 대신 rebase를 쓰면 히스토리를 일직선으로 유지할 수 있습니다.

merge 결과:
  main:   A → B → C → M
                     ↗
  feature: D → E

rebase 결과:
  main:   A → B → C → D' → E'
# feature 브랜치에서
git checkout feature
git rebase main

# 충돌이 없으면 자동으로 완료
# 충돌이 있으면 수동 해결 후:
git add .
git rebase --continue

# rebase 취소하고 싶으면:
git rebase --abort

주의: 이미 push한 커밋은 절대 rebase하지 마세요. 다른 사람의 히스토리가 꼬입니다.

충돌(Conflict) 해결

충돌이 뭔가요?

두 브랜치에서 같은 파일의 같은 부분을 다르게 수정하면 Git이 어느 쪽을 선택해야 할지 모릅니다.

git merge feature
# CONFLICT (content): Merge conflict in hello.py
# Automatic merge failed; fix conflicts and then commit the result.

충돌 마커 이해하기

파일을 열어보면 Git이 마커를 남깁니다:

def greet():
<<<<<<< HEAD
    print("안녕하세요!")        # 현재 브랜치(main)의 내용
=======
    print("Hello!")             # 병합하려는 브랜치(feature)의 내용
>>>>>>> feature

구조:

<<<<<<< HEAD
내 브랜치의 내용
=======
상대 브랜치의 내용
>>>>>>> 브랜치이름

충돌 해결 4단계

1단계: 충돌 파일 찾기

git status
# Unmerged paths:
#   both modified:   hello.py
#   both modified:   utils.py

2단계: 파일 열어서 수정

# 마커를 지우고 원하는 코드만 남기기
def greet():
    print("안녕하세요!")
    print("Hello!")

3단계: 해결 완료 표시

git add hello.py

4단계: 병합 완료

git commit
# (충돌 해결 커밋 메시지가 자동으로 작성됨, 그대로 저장)

충돌 해결 전략

전략 1: 수동으로 직접 수정

위의 4단계를 따라합니다. 가장 정확하지만 시간이 걸립니다.

전략 2: 한쪽을 통째로 선택

# 내 브랜치(현재) 버전으로 통째로 선택
git checkout --ours hello.py

# 상대 브랜치 버전으로 통째로 선택
git checkout --theirs hello.py

# 각각 add 후 commit
git add hello.py

전략 3: merge 전략 옵션 사용

# 충돌 시 항상 내 버전 선택
git merge feature -X ours

# 충돌 시 항상 상대 버전 선택
git merge feature -X theirs

주의: -X ours/theirs는 같은 파일의 같은 줄이 충돌했을 때만 적용됩니다. 한쪽에만 있는 변경은 자동 병합됩니다.

전략 4: 병합 취소하고 처음부터

# merge 취소
git merge --abort

# rebase 취소
git rebase --abort

# 상태 확인
git status
# (깨끗한 상태로 돌아옴)

VS Code에서 충돌 해결하기

VS Code는 충돌을 시각적으로 보여줍니다:

┌─────────────────────────────────┐
│ <<<<<<< HEAD         [Accept Current Change]
│     print("안녕하세요!")        │
│ =======                         │
│     print("Hello!")             │
│ >>>>>>> feature     [Accept Incoming Change]
│                    [Accept Both Changes]
│                    [Compare Changes]         │
└─────────────────────────────────┘

버튼 설명: - Accept Current Change: 내 브랜치 것만 유지 - Accept Incoming Change: 상대 브랜치 것만 유지 - Accept Both Changes: 둘 다 유지 - Compare Changes: 나란히 비교 화면 열기

해결 후 저장 → Source Control에서 커밋하면 됩니다.

복잡한 충돌 실전 예시

상황: 세 파일이 충돌났을 때

git merge feature
# CONFLICT (content): Merge conflict in app.py
# CONFLICT (content): Merge conflict in utils.py
# CONFLICT (content): Merge conflict in style.css

# 1. 상태 확인
git status

# 2. utils.py는 상대 버전으로 (내가 안 건드린 파일)
git checkout --theirs utils.py

# 3. style.css는 내 버전으로 (상대가 구버전 코드)
git checkout --ours style.css

# 4. app.py는 직접 수정 (둘 다 건드린 파일)
# 에디터로 열어서 마커 확인 후 수동 수정

app.py 충돌 내용:

class App:
    def __init__(self):
<<<<<<< HEAD
        self.db = Database("localhost")
        self.port = 8080
=======
        self.db = Database(os.getenv("DB_HOST"))
        self.debug = True
>>>>>>> feature

해결: 둘 다 반영

class App:
    def __init__(self):
        self.db = Database(os.getenv("DB_HOST", "localhost"))
        self.debug = os.getenv("DEBUG", "false") == "true"
        self.port = int(os.getenv("PORT", "8080"))
# 5. 모든 파일 add
git add app.py utils.py style.css

# 6. 병합 커밋
git commit

merge vs rebase 비교

              merge                  rebase
히스토리:    분기가 남음            일직선
복잡도:      병합 커밋 생김         히스토리 깔끔
안전성:      안전 (변경 없음)       커밋이 재작성됨
추돌:       한 번에 해결           커밋마다 해결할 수 있음

언제 뭘 쓰나요?

상황 추천
feature → main 병합 merge 또는 squash merge
내 feature 브랜치를 최신 main에 업데이트 rebase
이미 push한 커밋 절대 rebase 금지, merge
오픈소스 기여 rebase (프로젝트 정책 확인)

gh로 PR 병합하기

# 일반 merge
gh pr merge 12 --merge

# squash merge (추천)
gh pr merge 12 --squash

# rebase merge
gh pr merge 12 --rebase

# 병합 후 브랜치 삭제 (기본 동작)
gh pr merge 12 --squash --delete-branch

# 병합 후 브랜치 유지
gh pr merge 12 --squash --no-delete-branch

실습 미션

미션 1: Fast-forward 경험하기

mkdir merge-practice && cd merge-practice
git init
echo "Hello" > file.txt
git add . && git commit -m "initial"
git checkout -b feature
echo "World" >> file.txt
git add . && git commit -m "add world"
git checkout main
git merge feature
git log --oneline --graph

미션 2: 충돌 해결하기

# main에서 다른 내용 추가
echo "Main content" >> file.txt
git add . && git commit -m "main update"

# feature 브랜치에서 같은 파일 수정
git checkout -b conflict-branch
git checkout -b conflict-branch HEAD~1  # feature 병합 전으로
echo "Feature content" >> file.txt
git add . && git commit -m "feature update"

# main에서 병합 시도 → 충돌!
git checkout main
git merge conflict-branch

# 충돌 해결 후 커밋

미션 3: Squash merge 경험하기

git checkout -b squash-test
echo "1" >> file.txt && git add . && git commit -m "commit 1"
echo "2" >> file.txt && git add . && git commit -m "commit 2"
echo "3" >> file.txt && git add . && git commit -m "commit 3"
git checkout main
git merge --squash squash-test
git commit -m "squash: three commits into one"
git log --oneline  # 커밋 하나만 생긴 것 확인

이전 장: 08-git-troubleshooting.md 다음 장: 10-vscode-setup.md - VS Code 설치와 Git 연동