Git Rebase 또는 Merge? 팀에 맞는 전략을 선택하세요

1. 개요

git merge와 git rebase는 모두 두 브랜치 사이의 변경 사항을 통합하는 데 사용되지만, 매우 다른 커밋 히스토리를 생성합니다. 잘못된 전략을 선택하면 로그가 복잡해지고 리뷰가 어려워질 수 있으며, 최악의 경우 동료의 히스토리를 망칠 수도 있습니다. 이 글은 구체적인 예제를 통해 각 명령의 메커니즘을 이해하고, 언제 merge를 사용하고 언제 rebase를 사용해야 하는지, 그리고 팀에서 rebase를 사용할 때 가장 흔한 실수를 피하는 방법을 알려줍니다.

2. 예제 상황

main에서 feature/payment 브랜치를 만들어 작업 중이라고 가정합시다. 작업하는 동안 동료가 main에 몇 개의 커밋을 merge했습니다.

git log --oneline --graph --all
* c3f9a1d (main) PaymentValidator 추가
* 7b2e4aa (main) OrderService 버그 수정
| * 9d1a0ef (feature/payment) PaymentService.processPayment() 추가
| * 4f3c2bb (feature/payment) PaymentService 초기화
|/
* a1b2c3d 공통 베이스 커밋

두 브랜치가 a1b2c3d에서 갈라졌습니다. 이제 pull request를 만들기 전에 feature/payment를 main과 같은 수준으로 가져오고 싶습니다. 두 가지 방법이 있습니다: merge 또는 rebase.

3. Git Merge: 히스토리 유지, merge 커밋 생성

3.1. 사용 방법

git checkout feature/payment
git merge main

이 명령은 새로운 merge 커밋을 생성하며, 이는 feature/payment의 마지막 커밋과 main의 마지막 커밋, 두 개의 부모를 가집니다.

*   e5f6a7b (HEAD -> feature/payment) feature/payment를 main에 merge
|\
| * c3f9a1d (main) PaymentValidator 추가
| * 7b2e4aa (main) OrderService 버그 수정
* | 9d1a0ef PaymentService.processPayment() 추가
* | 4f3c2bb PaymentService 초기화
|/
* a1b2c3d 공통 베이스 커밋

3.2. 특징

  • 두 브랜치의 원래 히스토리가 100% 유지됩니다. 어떤 커밋도 수정되지 않습니다.
  • 두 브랜치가 만난 시점을 기록하는 추가적인 merge 커밋이 생성됩니다.
  • push된 브랜치에 대해 완전히 안전합니다 — 누구도 영향을 받지 않습니다.

주의: git merge는 존재하는 커밋의 SHA를 절대 수정하지 않습니다. 이것이 shared 브랜치에서 항상 안전한 이유입니다.

4. Git Rebase: 히스토리 재작성, 직선 브랜치 생성

4.1. 사용 방법

git checkout feature/payment
git rebase main

Rebase는 feature/payment의 각 커밋을 가져와서 (브랜치가 갈라진 지점인 a1b2c3d 이후), 이를 main의 맨 위에 “다시 재생”(replay)합니다. 결과적으로 SHA가 다른 완전히 새로운 커밋이 생성됩니다.

* 2a9e1cc (HEAD -> feature/payment) PaymentService.processPayment() 추가
* d8b4f0a PaymentService 초기화
* c3f9a1d (main) PaymentValidator 추가
* 7b2e4aa (main) OrderService 버그 수정
* a1b2c3d 공통 베이스 커밋

이제 히스토리가 직선(linear history)이며, merge 커밋이 없습니다.

4.2. 특징

  • 9d1a0ef와 4f3c2bb는 더 이상 존재하지 않습니다 — 2a9e1cc와 d8b4f0a로 대체되며, 새로운 SHA를 가집니다.
  • 로그가 깔끔하고 읽기 쉽고 git bisect가 용이합니다.
  • 브랜치를 다른 사람이 이미 pull했다면 위험합니다. 왜냐하면 그들은 이제 “사라진” 오래된 커밋을 가지고 있기 때문입니다.

5. Conflict 처리: 작업할 때 가장 큰 차이점

merge의 경우, conflict가 있으면 merge 커밋 위치에서 한 번만 해결하면 됩니다:

git merge main
# CONFLICT (content): PaymentService.java에서 merge conflict
# ... 파일 수정, 해결 ...
git add PaymentService.java
git commit

rebase의 경우, 많은 커밋이 있으면 replay 중에 각 커밋마다 conflict가 발생할 수 있습니다:

git rebase main
# 커밋 4f3c2bb에서 CONFLICT
# 파일 수정, 해결 후:
git add PaymentService.java
git rebase --continue
# 다음 커밋도 conflict가 있으면 반복

rebase를 중단하고 이전 상태로 돌아가려면:

git rebase --abort

주의: 10개 커밋이 있는 브랜치를 rebase하는 것은 때때로 conflict를 10번 해결해야 한다는 뜻입니다 — rebase 전에 squash하는 것이 유용한 이유입니다(섹션 7 참조).

6. 황금 규칙: 공유 중인 브랜치는 rebase하지 말 것

rebase를 사용할 때 가장 중요한 규칙입니다.

# main 브랜치를 5명이 이미 pull했음
git checkout main
git rebase feature/old-stuff   # ❌ 극히 위험함
git push --force origin main

Rebase가 모든 커밋에 대해 새로운 SHA를 생성하므로, push --force 후, main의 이전 버전을 pull한 사람은 remote와 히스토리가 분기됩니다(diverged history). 그들의 다음 pull은 오류가 발생하거나 수많은 중복 커밋이 생성됩니다.

error: failed to push some refs to 'origin'
hint: Updates were rejected because the tip of your current branch is behind

rebase 후 force-push를 해야 한다면, --force 대신 --force-with-lease를 사용하여 다른 사람의 커밋을 덮어쓰지 않도록 하세요:

git push --force-with-lease origin feature/payment

--force-with-lease는 마지막 fetch 이후 remote에 누군가 추가로 push한 커밋이 없는지 확인합니다. 있으면 이 명령은 무분별하게 덮어쓰지 않고 실패합니다.

황금 규칙: 개인 브랜치에서만 rebase하세요. 아직 공유되지 않았거나 당신만 작업하고 있는 브랜치입니다. 절대 main, develop, 또는 다른 shared 브랜치에 rebase하지 마세요.

7. Interactive rebase: merge 전에 히스토리 정리

rebase의 매우 실용적인 용도는 pull request를 만들기 전에 브랜치의 “쓰레기” 커밋을 정리하는 것입니다.

git checkout feature/payment
git rebase -i HEAD~3

에디터가 열립니다:

pick 4f3c2bb PaymentService 초기화
pick 9d1a0ef PaymentService.processPayment() 추가
pick 1c8f3aa typo 수정

마지막 두 줄을 squash로 변경하여 첫 번째 커밋으로 병합합니다:

pick   4f3c2bb PaymentService 초기화
squash 9d1a0ef PaymentService.processPayment() 추가
squash 1c8f3aa typo 수정

저장하면 Git이 새로운 커밋 메시지를 작성하도록 요청합니다. 결과: 3개의 산발적인 커밋이 1개의 깔끔하고 의미 있는 커밋으로 — PR을 열기 전에 완벽합니다. 리뷰어가 “typo 수정”, “wip”, “import 깜빡했네” 같은 커밋을 읽어야 할 필요가 없습니다.

8. 실용적인 전략: 둘 다 결합

대부분의 효율적인 팀은 절대적으로 하나를 선택하지 않고 상황에 따라 결합합니다:

상황 사용 추천
개인 브랜치를 최신 main으로 업데이트 rebase
PR 전에 산발적인 커밋 정리 rebase -i (squash)
검토가 완료된 feature 브랜치를 main에 merge merge (보통 PR을 통해, squash-merge 가능)
많은 사람이 함께 작업/pull한 브랜치 merge, 절대 rebase 금지
통합 시점의 흔적을 감사를 위해 유지해야 함 merge

실제로 흔한 워크플로:

# 1. PR 전: main을 rebase하여 업데이트, 나중의 conflict 회피
git checkout feature/payment
git rebase main

# 2. 필요하면 커밋 정리
git rebase -i main

# 3. 개인 브랜치 push (안전함 - 당신만 사용)
git push --force-with-lease origin feature/payment

# 4. PR을 통해 main에 merge (merge 커밋 또는 squash-merge, main을 rebase하지 않음)

이 방식은 feature/payment 로그를 깔끔하고 직선적이며 PR 리뷰하기 쉽게 유지하면서, main은 승인된 PR에서만 merge 커밋(또는 squash 커밋)을 받으므로 안전합니다.

9. Squash merge: 중간 솔루션

많은 플랫폼(GitHub, GitLab)에는 “Squash and merge” 버튼이 있습니다 — PR의 모든 커밋을 하나의 커밋으로 병합한 후 main에 merge합니다.

git checkout main
git merge --squash feature/payment
git commit -m "결제 처리 기능 추가 (#42)"

결과: main에는 정확히 하나의 새 커밋이 있고, merge 커밋이 없으며, 브랜치를 삭제하지 않았다면 feature/payment (remote의) 히스토리는 그대로 유지됩니다. 이것은 PR 워크플로를 사용하는 팀에서 가장 일반적인 선택입니다. main의 로그는 깔끔하면서도 원본 히스토리를 건드리지 않기 때문입니다.

10. 흔한 실수

  • Rebase 후 일반 push하기 (--force 없이): Git이 로그가 분기되었다고 오류를 냅니다. 일반적인 git pull로 “수정”하려고 하지 마세요 — 의도하지 않은 merge 커밋이 생성되어 방금 만든 직선 히스토리가 망가집니다.
  • Rebase하는 브랜치가 shared 브랜치라는 것을 깜빡함: 항상 git log origin/<branch>를 확인하여 다른 사람이 이미 이전 커밋에 의존하고 있는지 확인하세요.
  • Conflict 해결 후 --continue를 깜빡함: rebase 상태가 “중단”됩니다. git status가 당신이 rebase 중간에 있다고 명확하게 알려줍니다.
  • 개인 브랜치에서 계속 merge 사용: 로그가 작은 merge 커밋으로 가득 찼을 때 읽기 어렵습니다 — 여러 번 main을 브랜치에 merge하는 대신 rebase하여 업데이트하세요.
  • Squash 과다 사용: 관련 없는 많은 변경 사항을 하나의 커밋으로 병합하면 git bisect와 부분적인 git revert를 쓸 수 없게 됩니다.

11. 결론

merge는 안전하고 히스토리를 충실하게 유지하며 shared 브랜치에 적합합니다. rebase는 깔끔한 직선 히스토리를 만들지만 SHA를 재작성하므로 공유되지 않은 개인 브랜치에서만 사용할 수 있습니다. 실제로는 절대적으로 한쪽을 선택할 필요가 없습니다: 개인 브랜치를 업데이트하고 정리하려면 rebase를, PR을 통해 main에 통합하려면 merge(또는 squash-merge)를 사용하세요. 황금 규칙을 기억하세요 — 다른 사람이 의존하는 브랜치는 절대 rebase하지 말 것 — 그러면 팀에서 Git 관련 문제를 대부분 피할 수 있습니다.

gitrebasemergeversion-controlworkflow