Krong Dev.
아카이브 목록

git merge vs git rebase


금일 수업에서는 git과 github의 기초를 실습을 통해 배웠습니다.

github를 사용하는 가장 큰 이유는 협업과 버전 관리에 있습니다. 따라서 가장 중요한 작업은 ’merge’입니다.

merge를 했는데, default 브랜치와 맞지 않아 충돌이 일어난 경우, 다른 사람의 코드를 리뷰하기 위해 pr을 들어갔는데, 어디서부터 어디까지 구현했는지 보기 어려웠던 경험 등 다양한 방식으로 협업에 어려움을 겪은 경험이 있습니다.

그럴 때마다 각 상황에 맞게 ’merge’할 수 있는 방식인 ‘git merge’와 ‘git rebase’에 대해 적어보려고 합니다.

git merge : 두 브랜치를 합치는 가장 기본적인 방법

작동 방식 :

  • 브랜치 A에서 작업 중인데, 브랜치 B의 변경 내역을 가져와야 되는 상황입니다.
  • 이때 git merge를 사용하면 브랜치가 달라진 부분을 찾고 각 브랜치의 변경 사항을 확인하고 둘을 합치는 ‘merge’ commit을 만듭니다.
  • 이때 merge 커밋은 두 부모 commit을 갖게 되는데, 하나는 브랜치 A의 최신 커밋, 다른 하나는 브랜치 B의 최신 커밋입니다.
  • git log를 확인해 보면 브랜치가 갈라졌다가 다시 합쳐지는 모습이 그래프로 나타나는데요. 마치 실제 강물이 갈라졌다가 다시 합류하는 것처럼 보입니다.

git merge의 장점

  • 원본 변경 이력을 보존합니다. 즉, 실제로 작업이 일어난 순서대로 기록됩니다.
  • 두 브랜치가 언제 갈라졌고, 각 브랜치가 어떤 변경 사항을 갖고, 언제 합쳐졌는지 있는 모든 그대로 남아있습니다.
  • 안전하고 예측 가능합니다.
  • 이미 공유된 커밋은 절대 수정하지 않고, 기존 커밋들은 그대로 두고, 새로운 merge 커밋만 추가되기 때문에 다른 팀원의 작업에 영향을 주지 않습니다.
  • 만약에 merge 명령을 취소하고 싶다면, git revert 명령어로 merge 커밋을 되돌리면 됩니다.

git merge의 단점

  • merge를 자주 사용하면, 변경 이력이 복잡해집니다.
  • 특히 mono repo나 활발한 repo에서는 merge 커밋이 많이 생겨서 git log를 보기에 어려워질 수 있습니다.

git rebase : 브랜치가 원래 어디가 갈라져 왔는지, 그 시작점을 바꿉니다.

  • *rebase: 토대를 다시 잡는다
  • 즉, 새로운 시작점에서 다시 커밋을 쌓아 올리는 것이라고 하는데요.

예를 들어,

상황

  1. default 브랜치 -> feature 브랜치로 분기하여 작업합니다.
  2. 그 사이에 default 브랜치에 새로운 커밋이 추가되었습니다.

이때 git rebase를 실행하면 다음과 같은 순서로 진행됩니다.

  1. git은 feature 브랜치를 먼저 임시에 빼놓습니다.
  2. 그다음 feature 브랜치를 default 브랜치의 최신 커밋으로 옮깁니다.
  3. 그리고 아까 빼놨던 커밋들을 하나씩 다시 적용합니다.

마치 처음부터 최신 default 브랜치에서 작업했던 것처럼 변경 이력을 다시 적는 것인데요, 결과적으로 브랜치가 갈라지지 않고 모든 커밋이 일직선으로 나열됩니다. git log를 확인하면 default 브랜치 뒤에 커밋 기록들이 깔끔하게 이어져 있게 됩니다.

git rebase의 장점

깔끔한 선형 변경 이력 : 프로젝트의 발전 과정을 마치 한 권의 책처럼 순차적으로 읽을 수 있습니다. 각 커밋의 의미가 더 명확해짐 : merge commit이라는 ‘노이즈(Noise)’가 없기 때문에 모든 커밋이 실제 코드 변경을 담고 있어 변경 이력을 탐색하기가 훨씬 쉽습니다.

*interactive rebase : git rebase에 ‘-i’ 옵션을 주면, 변경 이력을 자유롭게 편집이 가능하다고 하는데요, 예를 들어 여러 개의 작은 커밋을 하나로 합칠 수 있는데, 이를 squash라고 합니다. 작업하면서 임시 커밋을 만들었다면 이 커밋들을 하나의 의미 있는 커밋으로 합칠 수 있습니다. 커밋 메시지 수정, 커밋 순서 변경, 특정 커밋 삭제 등이 가능하며, 이렇게 하면 PR의 커밋 기록이 보기 편하도록 깔끔하게 정리가 가능합니다.

이는 코드 리뷰 관점에서 큰 차이를 만들어냅니다. 리뷰어가 PR을 볼 때, 각 커밋이 논리적으로 의미 있는 단위로 나뉘어 있으면 변경 사항을 이해하기가 훨씬 쉬워집니다.

예를 들어,

첫 번째 커밋 : 새로운 API 엔드포인트 추가 두 번째 커밋 : 해당 엔드포인트의 테스트 코드 추가 세 번째 커밋 : 문서 업데이트 이런 식으로 명확하게 나뉘어 있다면, 리뷰어는 각 커밋을 순서대로 보면서 논리적 흐름을 따라갈 수 있습니다.

git rebase의 단점

  • 변경 이력을 다시 씁니다 : 이미 다른 사람과 공유한 브랜치를 rebase하면, 커밋의 해시값이 바뀝니다. 즉, 내용은 같아도 git 입장에서는 완전히 다른 커밋이 됩니다.
  • 이미 push한 브랜치를 rebase했다면, ‘-f’ 옵션을 줘서 강제로 push해야 합니다. 이게 위험한 이유는 다른 사람이 같은 브랜치에서 작업하고 있다면 그 사람의 작업이 꼬일 수 있기 때문입니다.

rebase의 원칙

  • 함께 사용하는 브랜치에서는 절대 rebase하면 안됩니다.
  • 특히 main, master, develop과 같은 default 브랜치에서는 절대 rebase하면 안 됩니다.
  • 충돌(conflict)이 발생하면 해결 과정이 더 복잡해질 수 있습니다. merge는 conflict가 1번 발생하지만, rebase는 여러 번에 걸쳐 발생할 수 있습니다. 대부분의 경우 1번만 해결하면 되지만, 여러 commit이 같은 파일을 수정했다면 여러 번 해결해야 합니다.
  • interactive rebase나 squash는 초보자에게 학습 곡선(learning curve)이 높다고 하는데요. 잘못 사용하면 커밋을 잃어버릴 수 있지만 복구는 가능합니다(git reflog). 하지만 이것마저도 러닝 커브가 높습니다.

# 시나리오 : 실제 git merge와 git rebase를 선택해야 하는 상황 feature 브랜치에서 작업하는 동안 default 브랜치에는 다른 팀원들의 PR이 계속 병합되고 있는 상황입니다. ex. 대규모 프로젝트

default 브랜치의 최신 변경 사항을 feature 브랜치에 주기적으로 업데이트해 줘야 합니다. PR을 merge한 이후의 상황이 병합 후에 치명적인 영향을 끼칠 수 있기 때문입니다. 이 상황에서는 ‘merge’와 ‘rebase’ 중 어떤 명령어를 사용해야 할까요?

git merge를 사용하면 default 브랜치와 동기화할 때마다 merge 커밋이 생깁니다. 그렇게 되면 리뷰어 입장에서 작업 내용이 실제로 어디서부터 어디까지 작업한 내용인지 헷갈립니다. 이를 여러 번 반복하면 PR의 커밋 히스토리가 매우 지저분해집니다.

반면에 git rebase를 사용하면 작업한 커밋이 default 브랜치의 최신 commit 위로 깔끔하게 올라갑니다. 그렇게 되면, PR의 커밋 히스토리가 깔끔해집니다. 리뷰어가 커밋 탭을 봤을 때 오직 작업자의 커밋 기록만 보이게 되는 것이죠. 게다가 interactive rebase까지 활용해서 커밋들을 논리적으로 의미 있는 단위로 정리하면 리뷰어가 noise 없이 핵심 변경 사항만 알 수 있습니다.

하지만 이미 push한 브랜치를 rebase할 경우 force push가 필요합니다. 페어 프로그래밍을 하거나 여러 명이 같은 브랜치에서 작업한다면 주의해야 합니다. 결국에는 팀 차원에서 rebase를 장려하면 PR 단계부터 커밋 히스토리를 정리하는 습관이 자연스럽게 길러지게 됩니다. 이는 좋은 코드 리뷰 경험으로 이어지고 성숙한 개발 문화를 자리 잡게 합니다.

하지만 절대 이런 선호를 강요하지는 않습니다. 현실적으로 모든 개발자가 rebase를 잘 사용하는 것이 아니기 때문입니다. 이건 트레이드 오프입니다. ‘PR 작성자의 편의를 생각할 것인가?’ 혹은 ‘리뷰어의 편의를 택할 것인가?’ 이 선택은 팀원들의 git 숙련도, 프로젝트의 성격, 코드 리뷰를 바라보는 관점에 따라 달라집니다.

# 다른 시나리오 : PR이 승인됐고, default 브랜치에 병합할 차례 이 단계에서의 선택은 프로젝트의 전체 변경 이력을 어떻게 관리할 것인가에 대한 전략적 결정입니다. github와 같은 소스 코드 관리 플랫폼에서 PR을 병합할 때, 보통 3가지 옵션을 볼 수 있습니다.

  1. 가장 일반적인 병합 방식 : merge commit을 만든다.

변경 이력이 완전히 보존됩니다. 즉, PR의 모든 커밋 기록이 default 브랜치의 커밋 기록에 남습니다. 이는 앞서 언급했듯이 로그가 정말 복잡해집니다.

  1. rebase & merge : PR의 커밋들을 default 브랜치 위로 rebase한 다음 병합하는 방식

default 브랜치의 변경 이력이 깔끔하면서도 세부 정보를 잃지 않습니다. 하지만 rebase 후 병합하면 default 브랜치에서 git log를 볼 때 PR 간의 경계가 모호해집니다. PR의 커밋들이 줄줄이 나열되어 있기 때문입니다.

  1. squash & merge : PR의 모든 커밋을 하나로 합쳐서 병합하는 방식입니다.

예를 들어 PR에 5개의 커밋이 있다면 이 5개가 하나의 커밋으로 압축됩니다. 이 방식으로 PR을 병합하면 default 브랜치에 커밋 히스토리가 정말 단순해집니다. default 브랜치의 각 log를 보면 각 커밋이 하나의 PR을 나타냅니다. 전체 프로젝트의 진행 과정을 빠르게 훑어보고 싶을 때 유용합니다.

하지만 PR 내부의 세부 변경 이력이 사라지기 때문에 큰 PR을 병합할 때는 단점이 될 수 있습니다. 버그가 발생하면 정확히 어느 시점에 코드가 추가되었는지 히스토리 추적이 어렵고, git revert로 기존 코드를 원복하기에도 까다롭습니다.

그렇다면 이 세 가지 동작 방식에서 어떤 방법을 선택해야 할까요? 정답은 없습니다.

결론 : 각 방식은 서로 다른 가치를 우선시하기 때문에 중요한 것은 팀 전체가 하나의 컨벤션을 정하고 일관성 있게 따르는 것입니다. 이러한 팀의 정책을 기술적으로 강제하는 방법은 저장소 설정에서 허용할 병합 방식을 지정하는 것입니다. PR 템플릿이나 기여 가이드에 병합 방식에 대한 팀의 방침을 명확히 적어야 합니다.

  • #배움일기
  • #Git