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 연동