Git

git rebase를 씁시다

date
May 5, 2024
thumbnail
git-rebase-thumbnail.png
slug
about-git-rebase
author
status
Public
tags
Git
Github
summary
브랜치 병합에는 크게 두 가지 방법, git merge와 git rebase
type
Post
category
Git
 
개발자 육성을 담당하는 많은 멘토들이 있는 채팅방에서 나왔다는 사진 한 장.
 
“사람은 git rebase를 씁니다.” ”git 강의 내용에 rebase, pull —rebase 쓰라는 내용이 있으면 좋겠네요.”
 
notion image
 
거대한 오픈소스 메인테이너, 커미터들이 있는 방에서 나온 말이라고 한다. 대체 git rebase를 사용하는 것에 어떤 장점들이 있길래 저렇게 까지 강조하고 계시는 걸까?
 

복잡한 커밋 히스토리가 야기하는 문제

notion image
위 사진은 20명의 인원이 참여한 원격 저장소의 Git Graph를 나타낸 사진이다.
 
20명이 했는데도 그래프가 이렇게 복잡해진다. 만약 큰 오픈소스에 참여한다면 어떨까? 예를 들어, 리눅스 커널 오픈소스의 경우 전 세계 수천 명의 엔지니어가 기여하고 있다. 고작 20명이 Git merge를 통해 브랜치를 병합할 경우 위와 같은 그래프가 나오는데, 그런 거대한 오픈 소스에서 위와 같은 커밋 히스토리를 남기게 되면 어떻게 될까? 아마 수천 줄의 폭을 가진 히스토리가 생성될 것으로 보인다.
 
그렇다면 커밋 히스토리가 복잡할 수록 어떤 단점이 생기는지 알아보자.
 
  • 커밋 히스토리의 의미가 감소한다.
    • 히스토리를 파악하기 어려워지며, 각 커밋이 어떤 변경을 나타내는지 명확하지 않게 되어 의미 있는 단위로 커밋을 관리하기 어렵다.
  • 커밋 그래프를 이해하기 어려워진다.
    • 커밋 그래프가 여러 갈래로 분기되면 시각적으로도 복잡해지며, 이는 프로젝트에 참여한 신규 엔지니어나 프로젝트 외부인이 히스토리를 이해하는데 큰 장애물이 된다.
  • 문제 추적이 어려워진다.
    • 버그가 생겼을 때, 복잡한 히스토리에서는 어떤 커밋이 어떤 변경 사항을 포함하고 있는지 명확하지 않기 때문에 문제의 원인을 찾기가 매우 어려워진다.
  • 리뷰 프로세스가 복잡해진다.
    • 코드 리뷰를 할 때 여러 갈래로 분기된 히스토리를 따라가며 리뷰하는 것이 매우 힘들어지며, 이는 코드 품질 관리를 어렵게 할 가능성이 있다.
 
그렇다면 rebase를 사용하면 커밋 그래프가 어떻게 될까?
 
 
notion image
Git Rebase를 하지 않고 여러 갈래로 분기한 브랜치를 Merge하여 관리하는 경우의 그래프
 
notion image
 
Git Rebase를 사용하여 커밋 그래프를 최대한 단순하게 유지하는 경우의 그래프
 
(이미지 출처: 우린 Git-flow를 사용하고 있어요 | 우아한형제들 기술블로그)
 
커밋 히스토리를 위와 같이 단순하게 유지할수록 커밋의 변경 사항의 명확성, 버그 추적의 용이성, 그리고 코드 리뷰의 효율성까지 개선할 수 있다. 이러한 이유로 오픈 소스에서 rebase로 정리된 히스토리가 아닌 merge commit으로 지저분해져 있는 PR은 있으면 잘 받아주지 않는다고 한다.
 

브랜치 병합


우리는 Git과 Github 원격 저장소를 사용한다. 요즘 개발자들이라면 사용하지 않는 것이 더 이상해보이기도 하고, 심지어 개발자가 아니어도 사용하는 게 깃허브다.
 
깃 허브 원격 저장소에서 내 PC인 로컬 환경으로 저장소를 복제(clone)하면, 복제를 했을 때 시점의 저장소가 로컬에 생긴다. 나 뿐만이 아니라 프로젝트에 참여하는 모두가 복제를 한다. 때문에 원격 저장소의 이름을 구분할 필요가 있다.
 
로컬 저장소의 main 브랜치와 원격 저장소의 main 브랜치를 구분하기 위해 원격 저장소를 origin이라는 별명을 붙여 구분한다. origin이 붙은 브랜치는 모두 원격 저장소에 있는 브랜치라는 뜻이다.
 
개발이 완료되면 PR을 올려서 코드 리뷰를 받고 원격 저장소의 브랜치로 병합(merge)한다. 이 작업은 나만 하는 것이 아니라 프로젝트에 참여한 모두가 진행한다. 이 때 로컬 환경과 원격 환경에는 이격(離隔, 사이가 벌어짐)이 발생한다.
 
원격 저장소(origin)에 새로운 커밋이 생긴다면 로컬 저장소와 이격이 발생한다.
원격 저장소(origin)에 새로운 커밋이 생긴다면 로컬 저장소와 이격이 발생한다.
 
애초에 이격이 있으면 새 커밋을 푸시할 수 없는 경우도 생긴다. 이격을 없애기 위해서는 로컬 저장소의 브랜치에 원격 저장소 브랜치의 코드 내용을 병합(merge)해야 한다. 이 과정에서 충돌을 해결하기도 하지만, 이것은 코드의 이격이다. 브랜치 간의 이격을 없애기 위해서는 병합을 해야 한다.
 

merge 병합 (fetch, merge, pull)


브랜치 병합 명령어 중 가장 많이 사용되는 것은 git merge 브랜치명이다. 그런데 초급 엔지니어들이 자주 헷갈려 하는 것이 바로 pull과 fetch, merge의 관계다.
 
git fetch
git fetch <가져오려는_브랜치_명>
fetch는 가져오기만 한다. 내가 작업하는 브랜치에 반영(병합)은 하지 않고 내용을 가져오기만 하는 것이다. git fetch 명령어를 진행하면 git graph에 새 커밋들이 생기는 것을 확인할 수 있다.
 
git merge
git merge <병합하려는_브랜치_명>
fetch로 가져온 내용을 현재 브랜치에 병합하는 명령어다. 현재 HEAD가 가리키고 있는, 작업하고 있는 브랜치를 기준으로 진행된다는 것을 주의해야 한다.
 
main 브랜치에서 원격 저장소의 main 브랜치에 있는 내용들을 main 브랜치로 반영하기 위해 다음과 같은 명령어를 입력한다.
main 브랜치에서 원격 저장소의 main 브랜치에 있는 내용들을 main 브랜치로 반영하기 위해 다음과 같은 명령어를 입력한다.
git pull origin main (git pull 저장소이름 브랜치이름)
21-gildong-feature 브랜치에서 원격 저장소의 main 브랜치에 있는 내용을 21-gildong-feature 브랜치에 반영하기 위해 다음과 같은 명령어를 입력한다.
21-gildong-feature 브랜치에서 원격 저장소의 main 브랜치에 있는 내용을 21-gildong-feature 브랜치에 반영하기 위해 다음과 같은 명령어를 입력한다.
git fetch origin
git merge origin/main
 
또한, git merge는 커밋을 발생시킨다. 이를 merge 커밋이라고 한다. merge가 완료되었다는 기록을 남기는 커밋이다. 만약 충돌이 발생하면 충돌을 해결한 뒤에 커밋을 만들고 병합을 완료할 수 있다.
 
git pull
git pull <원격_이름> <브랜치_명>
외우자. git pull은 fetch && merge를 진행하는 명령어다. git pull이 나오기 전에는 모두가 git fetch 후에 git merge를 하도록 두 번 명령어를 쳐야 했다. 이를 간소화하기 위해 나온 명령어다.
git fetch, git merge는 Git 1.5.0 버전에 나왔고, git pull은 Git 1.6.0 버전에 처음 나왔다.
 
 

rebase 병합


또 다른 병합 방법 중 하나인 리베이스(rebase)에 대해 알아보자.rebase는 re + base의 합성어다. base를 다시 지정하다는 뜻이다. 깃의 브랜치 병합 방법들 중 하나로, 기존에 분기된 브랜치에서 현재 최신 상태를 가지고 있는 base로 base를 다시 설정하겠다는 뜻이다.
 

base

기초, 토대, 근거지, 기반, 근거지, 본부, (군사)기지 - 영한사전
전투 비행기를 떠올려 보자. 오산에 공군 기지가 있고, 평택에도 공군 기지가 있다. 오산 base와 평택 base. 즉, 기지에는 전투기들이 있다. 전투기가 베이스를 오산에서 평택으로 변경할 때, 즉 전투기의 주둔지를 옮기는 것을 rebase라고 한다. (실제로 rebase라고 함)
 

git rebase

git rebase <병합하려는_브랜치_명>
리베이스를 하기 전에 fetch를 통해 업데이트된 내용을 가져와서 병합하는 것이 일반적이다. 이 과정을 빠르게 하려면 git pull --rebase를 사용하자.
 

리베이스 연습해보기

조금 오래되긴 했으나 git 명령어를 실시간으로 시각화한 모습을 보며 실습해볼 수 있는 사이트다.
 
notion image
(커밋을 체크아웃하면 detached head 상태가 된다.)
 
  • main(master) 브랜치가 있고, b9416d2… 커밋에서 분기된 브랜치 feature1가 있다.
  • feature1 브랜치에는 분기된 이후 4개의 커밋이 추가되었다.
  • feature1 브랜치에서 main(master)에 비해 3개의 커밋이 뒤쳐져 있는 상태다.
  • 이 상태에서 feature1 브랜치에 업데이트된 main(master)의 내용을 병합하려고 한다.
 
detached head 왜 브랜치 이름이 해쉬 값으로 나올까? 원격 저장소에 있는 브랜치를 체크아웃 하면, detached HEAD 상태가 되어 읽기 전용이기 때문에 수정하면 안된다. 수정하고 싶으면 브랜치 만들고 수정해야 한다. detached HEAD 상태란 말 그대로 HEAD가 브랜치로부터 떨어져 있는 상태를 뜻한다. 즉, 브랜치를 통해서가 아니라 직접 다이렉트로 commit을 참조하고 있는 상태를 뜻한다.
 
git rebase master(main)를 통해 feature1 브랜치를 업데이트된 main(master)로 리베이스해보자.
두 브랜치의 공통 조상을 바탕으로, feature1의 모든 커밋이 master로 주둔지를 옮기게 된다.
notion image
git merge로 feature 브랜치를 합쳤을 때는 commit ID가 생겼는데, rebase를 썼더니 그래프가 일자로 생성되고 merge 커밋이 생기지 않아 깔끔해진다.
 

주목할 부분

브랜치 전체를 뚝 떼다 붙이는 것이 아니다. 순서대로 보면 첫 커밋을 master에 붙이고, 그 다음 커밋을 붙이고.. 붙이고 해서 feature1의 HEAD를 마지막으로 붙이는 순서로 진행된다.
 
리베이스를 했더니 커밋 ID가 바뀌었다? 커밋의 내용도, 시간도 똑같은데 커밋 ID는 바뀌었다.
  • Git의 Commit ID는 SHA-256 (보안 해시 알고리즘)을 사용한 해시 값으로 만들어진다. (여기에는 부모 커밋, 시간 등을 조합하여 만들어진다.)
  • 그런데 리베이스를 진행하면 커밋들의 부모 커밋이 바뀌기 때문에 완전히 별개의 커밋 ID가 만들어진다.
 
 

rebase의 큰 단점

충돌 해결을 계속 해야 할 수 있다!
  • origin/main에 feature1을 붙일 때, 각 커밋을 붙이면서 발생하는 모든 충돌을 해결해야 한다.
  • worst case로는 한 번 해결한 충돌을 모든 커밋에 다 해결해줘야 하는 상황이 발생할 수도 있다.
    • 이럴 때는 interactive rebase를 사용해서 커밋을 합친 후, 리베이스 하고, 다시 커밋을 쪼갠다.
      git rebase —i ${수정할 커밋의 직전 커밋} or git rebase --interactive
      수정할 커밋의 직전 커밋 외에도 커밋 해시 혹은 HEAD를 이용할 수 있다.
 
 
깃허브에서 PR을 머지할 때 3가지 옵션
깃허브에서 PR을 머지할 때 3가지 옵션
notion image
  • Merge
  • Squash and Merge
  • Rebase and Merge
Merge 시점을 기록에 남기기 위해 일반적으로 업무에서는 Merge 사용하는 경우가 많다.
 

Reference