1. 들어가며
Git을 사용해 여러 개발자가 협업하다 보면 서로 다른 브랜치에서 작업한 코드를 하나로 통합해야 하는 상황이 발생한다.
이때 사용하는 대표적인 명령어가 git merge와 git rebase이다.
두 명령어는 모두 브랜치의 변경 사항을 통합하는 데 사용되지만, 기존 커밋 히스토리를 유지하는지, 새로운 기준점에서 재구성하는지에 차이가 있다.
이번 글에서는 Merge와 Rebase의 동작 원리, 차이점, 적절한 사용 시점과 주의사항을 살펴보려고 한다.
2. git merge란?
git merge는 서로 다른 브랜치에서 발생한 변경 사항을 하나로 통합하는 명령어이다.
가장 큰 특징은 기존 커밋 히스토리를 재작성하지 않고 유지한다는 점이다.
2.1. 동작 원리
다음과 같은 브랜치 상태를 가정하자.
Merge 이전
D --- E (feature/login)
/
A --- B --- C (main)
main: A → B → C 순서로 커밋이 생성되었다.feature/login: B 커밋에서 분기하여 D, E 커밋이 추가되었다.
로그인 기능 브랜치를 main에 병합하려면 다음 명령어를 실행한다.
git switch main
git merge feature/login
Merge 이후
D --- E
/ \
A --- B --- C --- M (main)
여기서 M은 새롭게 생성된 Merge Commit(병합 커밋) 이다.
Git은 기존 커밋 A, B, C, D, E를 그대로 유지하면서 두 브랜치의 변경 사항을 통합한 새로운 커밋 M을 생성한다.
일반적인 커밋은 하나의 부모 커밋을 가지지만, 이러한 Merge Commit은 두 개의 부모 커밋을 가진다.
따라서 어느 브랜치에서 어떤 작업이 진행되었고, 어느 시점에 병합되었는지 확인할 수 있다.
2.2. 3-Way Merge란?
위와 같이 두 브랜치가 서로 분기된 상태에서 병합할 때 Git은 일반적으로 3-Way Merge(3방향 병합) 방식을 사용한다.
Git은 다음 세 커밋을 기준으로 변경 사항을 비교한다.
| 구분 | 커밋 | 의미 |
|---|---|---|
| Base | B | 두 브랜치의 공통 조상 |
| Current | C | 현재 브랜치의 최신 커밋 |
| Target | E | 병합 대상 브랜치의 최신 커밋 |
공통 조상인 B를 기준으로 C와 E에서 각각 어떤 변경 사항이 발생했는지 비교한 후 이를 통합한다.
자동으로 통합할 수 없는 변경 사항이 있다면 충돌(Conflict)이 발생한다.
2.3. Fast-Forward Merge란?
git merge를 실행한다고 해서 항상 Merge Commit이 생성되는 것은 아니다.
다음과 같은 상황을 살펴보자.
Merge 이전
A --- B --- C (main)
\
D --- E (feature/login)
기능 브랜치가 생성된 이후 main에는 새로운 커밋이 추가되지 않았다.
이 상태에서 Merge를 실행하면 다음과 같이 변경된다.
A --- B --- C --- D --- E
↑
main
feature/login
새로운 Merge Commit을 생성하지 않고 main 브랜치의 포인터만 E 커밋으로 이동한다.
이를 Fast-Forward Merge(빨리 감기 병합) 라고 한다.
현재 브랜치가 병합 대상 브랜치의 조상이기 때문에 포인터를 이동하는 것만으로 변경 사항을 통합할 수 있다.
반대로 Fast-Forward가 가능한 상황에서도 Merge Commit을 남기고 싶다면 다음 옵션을 사용한다.
git merge --no-ff feature/login
--no-ff는 기능 단위의 병합 이력을 명확하게 기록하고 싶을 때 유용하다.
--ff-only 라는 옵션도 있는데 이 옵션은 Fast-Forward가 가능한 상황에서는 병합이 되고 불가능한 상황이라면 병합이 거부된다.
2.4. git merge는 언제 사용할까?
① 여러 개발자의 작업을 통합할 때
기능별 브랜치에서 개발을 완료한 후 main이나 develop 같은 공통 브랜치에 병합할 때 적합하다.
② 기존 커밋 히스토리를 보존해야 할 때
커밋이 생성된 순서와 브랜치가 병합된 과정을 유지하므로 작업 이력을 추적하기 쉽다.
③ 여러 개발자가 공유하는 브랜치를 관리할 때
기존 커밋을 재작성하지 않기 때문에 공유 브랜치에서 상대적으로 안전하게 사용할 수 있다.
다만 병합이 반복되면 Merge Commit이 누적되어 커밋 그래프가 복잡해질 수 있다.
3. git rebase란?
git rebase는 현재 브랜치의 커밋을 다른 브랜치의 최신 커밋을 기준으로 다시 적용하는 명령어이다.
Rebase는 커밋의 기반(Base)을 변경한다는 의미로, 기존 커밋을 새로운 기준점 위에 다시 적용하여 히스토리를 재구성한다.
이를 통해 직선적인 커밋 히스토리(Linear History)를 만들 수 있다.
3.1. 동작 원리
다음과 같은 상황을 가정하자.
Rebase 이전
D --- E (feature/login)
/
A --- B --- C (main)
로그인 기능을 개발하는 동안 main에 새로운 C 커밋이 추가되었다.
이때 최신 main의 변경 사항을 기능 브랜치에 반영하려고 다음 명령어를 실행한다.
git switch feature/login
git rebase main
Rebase 이후
A --- B --- C (main)
\
D' --- E' (feature/login)
Git은 다음 순서로 동작한다.
- 두 브랜치의 공통 조상인 B 커밋을 찾는다.
- 기능 브랜치에만 존재하는 D, E 커밋의 변경 사항을 확인한다.
main의 최신 커밋 C를 새로운 기준점으로 설정한다.- D, E의 변경 사항을 순서대로 다시 적용하여 D’, E’ 커밋을 생성한다.
결과적으로 기능 브랜치의 히스토리는 다음과 같이 정리된다.
A --- B --- C --- D' --- E'
중요한 점은 기존 D, E 커밋이 단순히 이동한 것이 아니라 새로운 커밋 D’, E’로 재작성되었다는 것이다.
Git의 커밋 ID는 부모 커밋 등의 정보를 기반으로 생성되므로, Rebase로 부모 커밋이 변경되면 커밋 ID도 달라진다.
단, 이미 적용된 변경 사항이나 빈 커밋을 처리하는 과정에서 일부 커밋이 생략될 수도 있다.
3.2. git rebase는 언제 사용할까?
① 개인 기능 브랜치에 최신 main을 반영할 때
기능을 개발하는 동안 다른 개발자가 main에 새로운 변경 사항을 반영했다면 다음과 같이 사용할 수 있다.
git switch feature/login
# 원격 저장소 최신 정보 가져오기
git fetch origin
# 최신 원격 main을 기준으로 Rebase
git rebase origin/main
git fetch origin으로 원격 추적 브랜치를 갱신한 후 origin/main을 기준으로 Rebase를 수행한다.
이를 통해 최신 main의 변경 사항 위에 자신의 커밋을 다시 적용할 수 있다.
② Pull Request 생성 전에 커밋을 정리할 때
기능 개발 과정에서 다음과 같은 커밋이 생성되었다고 가정하자.
A --- B --- C --- D
- B: 로그인 API 구현
- C: 오타 수정
- D: 로그인 API 예외 처리 수정
Interactive Rebase를 활용하면 세 개의 커밋을 하나로 정리할 수 있다.
git rebase -i HEAD~3
편집 화면에서 다음과 같이 변경한다.
pick <B의 해시> 로그인 API 구현
squash <C의 해시> 오타 수정
squash <D의 해시> 로그인 API 예외 처리 수정
squash는 해당 커밋을 이전 커밋과 합치는 명령어이다.
결과적으로 다음과 같이 정리할 수 있다.
A --- B'
이를 통해 불필요한 수정 커밋을 줄이고 PR의 변경 사항을 이해하기 쉽게 만들 수 있다.
3.3. git rebase 사용 시 주의사항
Rebase는 기존 커밋을 다시 작성하기 때문에 다른 개발자가 의존하는 공유 커밋에 함부로 사용해서는 안 된다.
다른 개발자가 이미 해당 커밋을 기준으로 작업하고 있다면, Rebase 이후 커밋 ID가 변경되면서 히스토리 불일치가 발생할 수 있다.
따라서 개인 브랜치에서는 유용하지만 공유 브랜치에서는 주의해야 한다.
이미 Push한 개인 브랜치를 Rebase했다면 일반적인 Push가 거부될 수 있다.
팀에서 히스토리 재작성을 허용하는 경우에는 다음 명령어를 사용할 수 있다.
git push --force-with-lease
--force-with-lease는 예상한 원격 참조 상태가 유지되고 있는지 확인한 뒤 Push하므로 일반적인 --force보다 안전하다.
다만 다른 개발자의 작업을 항상 보호하는 것은 아니므로 신중하게 사용해야 한다.
4. Merge와 Rebase에서 충돌이 발생한다면?
Merge와 Rebase는 모두 변경 사항을 통합하는 과정에서 충돌(Conflict)이 발생할 수 있다.
예를 들어 두 브랜치에서 동일한 코드를 서로 다르게 수정했다고 가정하자.
main 브랜치
public String getMessage() {
return "Hello";
}
feature/login 브랜치
public String getMessage() {
return "Welcome";
}
Git이 두 변경 사항을 자동으로 통합하지 못하면 다음과 같은 충돌 표시가 나타날 수 있다.
public String getMessage() {
<<<<<<< HEAD
return "Hello";
=======
return "Welcome";
>>>>>>> feature/login
}
개발자는 어떤 코드를 유지할지 결정한 후 충돌 표시를 제거해야 한다.
Merge 충돌 해결
git status
# 충돌 파일 수정 후
git add <파일명>
git merge --continue
병합을 취소하려면 다음 명령어를 사용한다.
git merge --abort
Rebase 충돌 해결
git status
# 충돌 파일 수정 후
git add <파일명>
git rebase --continue
Rebase를 취소하려면 다음 명령어를 사용한다.
git rebase --abort
Rebase는 커밋을 순서대로 다시 적용하므로 여러 커밋에서 충돌이 반복될 수 있다.
또한 Rebase에서는 충돌 표시의 HEAD와 ours, theirs가 가리키는 변경의 의미가 일반적인 Merge와 다르게 해석될 수 있으므로 주의해야 한다.
충돌을 해결한 후에는 코드가 정상적으로 동작하는지 테스트하는 것이 중요하다.
5. git merge vs git rebase 비교
| 비교 항목 | git merge | git rebase |
|---|---|---|
| 목적 | 브랜치 변경 사항 통합 | 커밋의 기준점 변경 및 재적용 |
| 동작 방식 | 기존 커밋을 유지하면서 병합 | 새로운 기준점에 커밋 재적용 |
| 기존 커밋 ID | 유지 | 재작성된 커밋은 변경 |
| Merge Commit | 필요한 경우 생성 | 일반적인 Rebase에서는 생성하지 않음 |
| 커밋 히스토리 | 분기 및 병합 이력 보존 | 직선적인 히스토리 구성 가능 |
| 장점 | 작업 이력 추적 및 협업에 유리 | 깔끔한 커밋 히스토리 |
| 단점 | 병합 커밋이 누적될 수 있음 | 공유 커밋 재작성 시 문제 발생 가능 |
| 적절한 상황 | 공유 브랜치 및 기능 통합 | 개인 브랜치 최신화 및 커밋 정리 |
두 명령어의 핵심 차이는 기존 커밋 히스토리를 유지하는지, 새로운 기준점에서 재구성하는지에 있다.
6. 마무리
git merge와 git rebase는 모두 브랜치의 변경 사항을 통합하는 명령어이지만, 커밋 히스토리를 처리하는 방식이 다르다.
Merge는 기존 커밋을 유지하면서 변경 사항을 통합하므로 공유 브랜치나 기능 브랜치를 병합할 때 유용하다.
반면 Rebase는 커밋을 새로운 기준점 위에 다시 적용하여 히스토리를 정리하므로 개인 기능 브랜치를 최신화하거나 PR을 생성하기 전에 커밋을 정리할 때 적합하다.
결국 중요한 것은 어떤 명령어가 더 좋은지가 아니라, 브랜치가 공유되고 있는지와 어떤 커밋 히스토리를 유지하려는지에 따라 적절한 방식을 선택하는 것이다.