1 Merge와 Rebase 개요
Git에서 브랜치를 통합하는 방법은 크게 두 가지다:
- Merge: 두 브랜치의 이력을 합쳐서 새로운 병합 커밋을 만든다
- Rebase: 한 브랜치의 커밋들을 다른 브랜치 위에 재배치한다
2 Git Merge
2.1 Fast-Forward Merge
main 브랜치에 새로운 커밋이 없고, feature 브랜치만 앞서 있을 때 발생한다.
병합 전:
main: A --- B
\
feature: C --- D
병합 후 (fast-forward):
main: A --- B --- C --- D
Updating b7e2c3a..f1d4a8e
Fast-forward
src/login.js | 47 ++++++++++++++++++++++++++++++++++
1 file changed, 47 insertions(+)
Fast-forward 와 함께 파일 변경 통계만 출력된다. 병합 커밋이 생성되지 않아 이력이 직선으로 유지된다.
2.2 3-Way Merge
main과 feature 브랜치 양쪽에 새로운 커밋이 있을 때 발생한다.
병합 전:
main: A --- B --- E
\
feature: C --- D
병합 후:
main: A --- B --- E --- M (병합 커밋)
\ /
feature: C --- D
Merge made by the 'ort' strategy.
src/login.js | 47 ++++++++++++++++++++++++++++++++++
1 file changed, 47 insertions(+)
Merge made by the 'ort' strategy. (또는 구버전 Git에서는 recursive)가 나오면 병합 커밋이 생성된 것이다.
--no-ff 옵션: Fast-forward가 가능한 상황에서도 병합 커밋을 강제로 생성한다.
# 편집기가 열리며 커밋 메시지를 입력한다 (기본 메시지가 채워져 있음):
# Merge branch 'feature/login'
# 저장 후 종료하면:
Merge made by the 'ort' strategy.
src/login.js | 47 +++++++++++++++
1 file changed, 47 insertions(+)
--no-ff 는 Fast-forward가 가능해도 편집기를 열어 병합 커밋 메시지 입력을 요구한다. feature 브랜치의 존재가 이력에 명확히 남는다.
2.3 Merge의 장단점
| 장점 | 단점 |
|---|---|
| 이력이 있는 그대로 보존됨 | 병합 커밋이 많아지면 이력이 복잡해짐 |
| 안전함 (기존 커밋 변경 없음) | 그래프가 복잡해질 수 있음 |
| 되돌리기 쉬움 |
3 Git Rebase
3.1 기본 Rebase
Rebase는 feature 브랜치의 커밋들을 main 브랜치의 최신 커밋 뒤에 재배치한다.
Rebase 전:
main: A --- B --- E
\
feature: C --- D
Rebase 후:
main: A --- B --- E
\
feature: C' --- D'
Successfully rebased and updated refs/heads/feature/login.
충돌 없이 완료되면 이 한 줄만 출력된다. 커밋이 여러 개면 각 커밋이 재적용되는 메시지가 나오기도 한다:
Rebasing (1/3)
Rebasing (2/3)
Rebasing (3/3)
Successfully rebased and updated refs/heads/feature/login.
C와 D가 C’와 D’로 바뀐 이유: Rebase는 커밋을 복사하여 새로운 위치에 놓는다. 원본 커밋(C, D)과 rebase된 커밋(C’, D’)은 내용은 같지만 해시값이 다른 별개의 커밋이다.
Rebase 후 main에 Fast-Forward Merge:
최종 결과:
main: A --- B --- E --- C' --- D'
이력이 일직선으로 깔끔하다.
3.2 Rebase의 장단점
| 장점 | 단점 |
|---|---|
| 이력이 일직선으로 깔끔함 | 커밋 해시가 변경됨 (이력 재작성) |
| 불필요한 병합 커밋 없음 | 공유된 브랜치에서 사용하면 위험 |
| 코드 리뷰가 수월함 | 충돌 해결이 커밋마다 필요할 수 있음 |
3.3 Rebase 중 충돌 해결
Rebase 도중 충돌이 발생하면 커밋 단위로 충돌을 해결해야 한다.
충돌이 발생하면:
Auto-merging src/login.js
CONFLICT (content): Merge conflict in src/login.js
error: could not apply abc1234... feat: Add login form
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply abc1234... feat: Add login form
could not apply <hash>... 가 나오면 해당 커밋을 재배치하는 과정에서 충돌이 발생한 것이다.
[detached HEAD f9a2b3c] feat: Add login form
1 file changed, 12 insertions(+)
Successfully rebased and updated refs/heads/feature/login.
모든 커밋에서 충돌이 해결되면 Successfully rebased 메시지가 나타난다.
Successfully aborted rebase.
4 Interactive Rebase
여러 커밋을 정리(합치기, 순서 변경, 메시지 수정 등)할 때 사용한다.
편집기가 열리며 다음과 같은 화면이 나타난다:
pick abc1234 feat: Add login form
pick def5678 fix: Fix login form typo
pick ghi9012 feat: Add login validation
# Commands:
# p, pick = 커밋 사용
# r, reword = 커밋 메시지 변경
# e, edit = 커밋 수정
# s, squash = 이전 커밋과 합치기 (메시지 합침)
# f, fixup = 이전 커밋과 합치기 (이 커밋 메시지 버림)
# d, drop = 커밋 삭제
4.1 실전 예시: 커밋 합치기 (Squash)
작업 중 생긴 자잘한 커밋들을 하나로 합친다:
pick abc1234 feat: Add login form
squash def5678 fix: Fix login form typo
squash ghi9012 feat: Add login validation
저장 후 종료하면 합쳐진 커밋의 메시지를 입력하는 두 번째 편집기가 열린다:
# This is a combination of 3 commits.
# The first commit's message is:
feat: Add login form
# This is the 2nd commit message:
fix: Fix login form typo
# This is the 3rd commit message:
feat: Add login validation
원하는 최종 메시지를 작성하고 저장하면:
[detached HEAD 7d3a1b2] feat: Add login form and validation
Date: Mon Jan 15 14:30:00 2024 +0900
3 files changed, 62 insertions(+), 2 deletions(-)
Successfully rebased and updated refs/heads/feature/login.
3개의 커밋이 하나의 커밋으로 합쳐진다.
4.2 실전 예시: 커밋 메시지 수정
pick abc1234 feat: Add login form
reword def5678 오타 수정 → 더 나은 메시지로 변경
pick ghi9012 feat: Add login validation
5 git pull –rebase
git pull은 기본적으로 fetch + merge를 수행한다. --rebase 옵션을 사용하면 fetch + rebase를 수행한다.
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
Unpacking objects: 100% (3/3), done.
From https://github.com/username/repo
a123456..b789012 main -> origin/main
Merge made by the 'ort' strategy.
README.md | 2 ++
1 file changed, 2 insertions(+)
원격 변경사항과 로컬 변경사항이 Merge made by the 'ort' strategy. 와 함께 병합 커밋으로 합쳐진다.
Successfully rebased and updated refs/heads/main.
로컬 커밋이 원격의 최신 커밋 뒤에 재배치되고 병합 커밋이 생기지 않는다.
기본 pull 전략을 rebase로 설정:
6 Merge vs Rebase: 선택 기준
6.1 Merge를 사용해야 할 때
- 공유 브랜치 (main, develop)에 통합할 때
- 이력 보존이 중요한 프로젝트
- 팀원이 Git에 익숙하지 않은 경우
- Pull Request 기반 워크플로우
6.2 Rebase를 사용해야 할 때
- 개인 feature 브랜치를 최신 상태로 유지할 때
- PR 전에 커밋 이력을 정리할 때
- 깔끔한 이력을 선호하는 프로젝트
6.3 핵심 규칙
이미 원격에 push한 커밋은 rebase하지 않는다.
Rebase는 커밋 해시를 변경하므로, 다른 팀원이 이미 해당 커밋을 기반으로 작업하고 있다면 심각한 충돌이 발생한다.
- 로컬에서만 작업한 커밋 → Rebase OK
- 이미 push한 커밋 → Merge 사용
6.4 실무에서 많이 쓰는 패턴
# 1. feature 브랜치에서 작업
git switch feature/login
# 2. main의 최신 변경사항을 rebase로 가져오기
git fetch origin
git rebase origin/main
# 3. 충돌 해결 후 push (force 필요 - 개인 브랜치에서만!)
git push --force-with-lease origin feature/login
# 4. PR 생성 → 코드 리뷰 → main에 merge성공 시:
To https://github.com/username/repo.git
+ a123456...f9a2b3c feature/login -> feature/login (forced update)
forced update 가 나타나면 원격 브랜치가 강제로 업데이트된 것이다. --force-with-lease 를 사용했기 때문에 다른 사람이 먼저 push한 경우에는 아래처럼 거부된다:
error: failed to push some refs to 'https://github.com/username/repo.git'
hint: Updates were rejected because the remote contains work that you do not have locally.
이 경우 git fetch 후 상황을 재확인해야 한다.
--force-with-lease는 --force보다 안전하다. 다른 사람이 해당 브랜치에 push한 내용이 있으면 거부되므로 실수로 다른 사람의 작업을 덮어쓰는 것을 방지한다.
7 요약 비교
| 항목 | Merge | Rebase |
|---|---|---|
| 이력 형태 | 비선형 (분기/합류) | 선형 (일직선) |
| 기존 커밋 변경 | 변경 없음 | 커밋 해시 변경됨 |
| 병합 커밋 | 생성됨 | 생성 안 됨 |
| 안전성 | 높음 | 주의 필요 |
| 충돌 해결 | 한 번에 처리 | 커밋 단위로 처리 |
| 적합한 상황 | 공유 브랜치 통합 | 개인 브랜치 정리 |