Published on

Git Rebase Overview

Authors

Git에서 브랜치를 병합하는 방법은 두 가지가 있습니다. 첫 번째는 Merge이고 다른 하나는 Rebase입니다. 이번 포스팅에서는 Rebase가 무엇인지, 어떻게 사용하는지, 좋은 점은 무엇인지, 어떤 상황에서 사용하고 어떤 상황에서 사용하지 말아야 하는지 알아보도록 합시다.

Rebase 기초

다음과 같은 브랜치의 모습을 가정해보겠습니다.

두 개의 브랜치로 나누어진 커밋 히스토리(출처: Pro Git Book)

두 개의 브랜치로 나누어진 커밋 히스토리(출처: Pro Git Book)

위 두 브랜치를 합치는 가장 쉬운 방법은 이전에 배웠던 merge 명령을 사용하는 것입니다. 두 브랜치의 마지막 커밋(C3, C4)과 공통 조상(C2)을 사용하는 3-way merge로 다음과 같은 새로운 커밋을 만들냅니다.

master 브랜치와 experiment 브랜치를 merge하기
master 브랜치와 experiment 브랜치를 merge하기

비슷한 결과를 만드는 다른 방식으로, C3에서 변경된 사항을 패치(Patch)로 만들고 이를 다시 C4에 적용시키는 방법이 있습니다. Git 에서는 이러한 방식을 Rebase라고 합니다. Rebase는 한 브랜치에서의 변경 사항을 다른 브랜치에 적용시키는 merge 와는 또 다른 방법입니다. 위의 예제는 다음과 같은 명령으로 Rebase 합니다.

> git checkout experiment
Switched to branch 'experiment'

> git rebase master
First, rewinding head to replay your work on top of it...
Applying: Add fourth line on experiment

여기서 실제로 일어나는 일을 설명하자면 일단 두 브랜치가 나뉘기 전인 공통 커밋(C2)으로 이동하고 나서 그 커밋에서부터 현재 위치하는 브랜치가 가리키는 커밋까지 diff를 차례로 만들어 임시로 저장해 놓습니다. Rebase 할 브랜치(여기서는 experiment)가 합칠 브랜치(여기서는 master)가 가리키는 커밋을 가리키게 하고, 아까 저장해 놓았던 변경사항들을 차례대로 적용시킵니다. 아래 그림은 이러한 과정을 나타내고 있습니다.

C3의 변경사항을 C4에 적용하는 rebase 과정
C3의 변경사항을 C4에 적용하는 rebase 과정

그리고 나서 master 브랜치를 Fast-forward 시켜줍니다.

master 브랜치를 Fast-forward 시키기
master 브랜치를 Fast-forward 시키기

C3'로 표시된 커밋에서의 내용은 그림 2의 C5 커밋에서의 내용과 같습니다. Merge이든 rebase든 둘 다 합치는 관점에서는 서로 다를 게 없습니다. 하지만 rebase 가 좀 더 깨끗한(선형적인) 커밋 히스토리를 만들어줍니다. 즉, rebase 하고 나면 모든 작업이 차례대로 수행된 것처럼 보이게 됩니다.

rebase는 보통 리모트 브랜치에서 커밋을 깔끔하게 적용하고 싶을 때 사용합니다. 즉 로컬 브랜치에서 작업한 내용을 origin/master 로 rebase 하여 리모트의 master 브랜치를 Fast-forward 시키면 됩니다.

rebase를 하든지, merge를 하든지 결과물은 동일하고 커밋 히스토리만 다르다는 것이 중요한 포인트입니다. Rebase의 경우 브랜치의 변경사항을 순서대로 다른 브랜치에 적용하면서 합치고 merge의 경우는 두 브랜치의 최종 결과만을 가지고 합치게됩니다.

Rebase 활용

Rebase는 단순히 브랜치를 합치는 것만 아니라 다른 용도로도 사용할 수 있습니다. 다음과 같이 다른 브랜치에서 또 갈라져 나온 브랜치 히스토리가 있다고 가정해보죠.

다른 브랜치(server)에서 또 갈라져 나온 브랜치(client)

다른 브랜치(server)에서 또 갈라져 나온 브랜치(client)

여기서 server 브랜치는 테스트가 덜 되어서 그대로 두고 client 브랜치만 master에 병합하려는 상황을 생각해보죠. server 와는 아무 상관이 없는 client 커밋은 C8, C9입니다. 이 두 커밋을 master 브랜치에 적용하기 위해서 --onto 옵션을 사용하여 아래와 같은 명령을 실행해줍니다.

git rebase --onto master server client

위 명령은 client 브랜치로 checkout 하고 serverclient의 공통조상 이후의 커밋의 패치를 만들어 master에 적용시킵니다.

다른 브랜치에서 갈라져 나온 client 브랜치를 master 브랜치에 rebase하기

다른 브랜치에서 갈라져 나온 client 브랜치를 master 브랜치에 rebase하기

이제 master 브랜치로 checkout 하여 Fast-forward 시키면 다음과 같습니다.

git checkout master
git merge client
 master 브랜치를 Fast-forward 시키기
master 브랜치를 Fast-forward 시키기

자, 이제 server 브랜치의 작업이 완료되었다고 해보죠. 우리는 다시 server 브랜치로 checkout 할 필요 없이 다음 명령으로 basebranchtopicbranch를 rebase 할 수 있습니다.

git rebase [basebranch] [topicbranch]

즉, 여기서는 basebranchmaster이고 topicbranchserver브랜치입니다. 다음 명령은 topicbranch(server)로 checkout 하고 basebranch(master)로 rebase 합니다.

git rebase master server

위 명령의 결과는 다음과 같습니다.

master 브랜치에 server 브랜치의 수정사항을 적용
master 브랜치에 server 브랜치의 수정사항을 적용

그리고 나서 master 브랜치를 다시 Fast-forward 시켜줍니다.

git checkout master
git merge server

자, 이제 master 브랜치의 모든 변경 사항이 적용되었습니다. 임무 수행을 마친 브랜치는 삭제해도 되죠. 브랜치를 삭제하고 난 후 커밋 히스토리는 다음과 같습니다.

git branch -d client
git branch -d server
최종 커밋 히스토리
최종 커밋 히스토리

Rebase의 위험성

Rebase는 브랜치를 병합할 때 merge와는 달리 커밋 이력이 존재하여 눈으로 변경 내용을 좀 더 명확하게 확인할 수 있고, 선형적인 히스토리를 만들어 준다는 장점이 있습니다. 하지만, 하지만 Rebase 는 잘못 사용하면 치명적인 오류를 가져올 수 있는데요, 바로 이미 공개 저장소에 push 한 커밋을 rebase 하는 경우 입니다.

이미 공개 저장소에 push한 커밋을 rebase 하면 어떤 결과를 초래하는지 예제를 통해 알아보도록 해봅시다. 리모트 레포지토리를 clone 하고 일부 수정을 하면 커밋 히스토리는 다음과 같습니다.

저장소를 clone 하고 일부 수정한 상태
저장소를 clone 하고 일부 수정한 상태

이제 팀원 중 누군가 리모트를 클론하여 브랜치를 파서 작업한 다음, merge 하고 서버에 push 하였다고 해보죠. 이를 다시 나의 로컬 레포지토리에 pull 하면 커밋 히스토리는 다음과 같게 됩니다.

팀원이 작업한 후 push 한 remote 를 pull 받은 상태

팀원이 작업한 후 push 한 remote 를 pull 받은 상태

그런데 push 했던 팀원이 merge 했던 커밋을  되돌리고 다시 rebase 하려고 한다고 생각해봅시다. 서버의 히스토리를 덮어 씌우려고 git push --force 명령을 사용하여 리모트에 push 하였다고 가정하면 커밋 히스토리는 다음과 같게 됩니다.

한 팀원이 우리가 의존하는 커밋을 없애고 rebase 한 후 push 함

한 팀원이 우리가 의존하는 커밋을 없애고 rebase 한 후 push 함

현재 C7이 참조하고 있는 커밋(C6)이 사라졌기 때문에 이미 처리한 일이라고 해도 또다시 merge 해야 합니다. Rebase는 커밋의 SHA 해쉬값을 바꾸기 때문에 Git은 새로운 커밋으로 C4'를 받아들이게 됩니다. 사실 C4는 이미 히스토리에 적용되어 있지만, Git은 이를 알지 못하게 되는 것이죠.

같은 merge를 다시 반복한다
같은 merge를 다시 반복한다

git log 명령으로 히스토리를 확인해 보면 저자, 커밋 날짜, 메시지가 동일하지만 SHA 해쉬 값이 다른 커밋이 두 개가 있게됩니다(C4, C4'). 이렇게 되면 혼란스럽죠. 게다가 이 히스토리를 서버에 push 하면 같은 커밋이 두 개가 존재하기 때문에 협업하는 사람들도 혼란스러워 할 수 밖에 없습니다. C4와 C6는 포함되지 말아야 할 커밋이지만, rebase 하기 전에 이미 다른 사람들이 해당 커밋을 참조하기 때문에 이러한 사고가 발생하게 된 것이죠.

이러한 사고가 발생한 경우에 대응할 수 있는 해결책이 없는것은 아닙니다. 여기에서 그 방법을 찾을 수 있습니다. 가장 좋은 방법은, 위에서 설명하였다시피 이미 공개하여 사람들이 사용하는 커밋을 rebase 하지 않는 것입니다.

Rebase vs Merge

자, 지금까지 rebase가 무엇인지, merge와는 어떻게 다른지 기나긴 여정을 지나 살펴보았습다. 지금쯤 한 가지 의문이 들 수 있을 것인데요, 과연 둘 중 무엇을 쓰는 게 좋은 것일까요? 이 질문에 대한 답을 찾기 전에 히스토리의 의미에 대해서 잠깐 다시 생각해보겠습니다.

커밋 히스토리를 보는 관점 중에 하나는 작업한 내용의 기록으로 보는 것이 있습니다. 커밋 히스토리는 작업 내용을 기록한 문서이고, 각 기록은 각각의 의미를 지니며, 변경할 수 없습니다. 이런 관점에서 커밋 히스토리를 변경한다는 것은 역사를 부정하게되는 꼴이죠. 언제 무슨 작업을 했었는지 기록에 대해 거짓말을 하게 되는 것입니다. 커밋의 역사는 보존되어야 할 필요가 있다는 관점에서 본다면 rebase 는 부정적으로 여겨지게 됩니다.

그러나 히스토리를 프로젝트가 어떻게 진행되었나에 대한 관점으로도 생각해 볼 수 있다. 소프트웨어를 주의 깊게 편집 하는 방법에 매뉴얼이나 세세한 작업내용을 초벌부터 공개하고 싶지 않을 수 있는데요. 나중에 다른 사람에게 보여주기 좋도록 rebase나 filter-branch 같은 도구로 프로젝트의 진행 이야기를 다듬으면 프로젝트가 진행된 이력을 사용자들에게 더 잘 전달할 수 있을 것이라고 긍정적으로 생각할 수 있습니다.

merge나 rebase 중 무엇이 좋냐는 질문은 다시 생각해봐도 섣불리 대답하기 어렵습니다. Git은 매우 유용해서 커밋 히스토리를 잘 쌓을 수 있지만, 모든 팀과 모든 개발자가 처한 상황은 다르기 때문에, 이 둘을 어떻게 사용할 것인지는 각자의 상황과 각자의 판단에 달려있다고 볼 수 있습니다.

위 질문의 해답을 일반화 시키자면 로컬 브랜치에서 작업할 때는 히스토리를 정리하기 위해 rebase 할 수도 있지만, 리모트 등 어딘가에 push 한 커밋에 대해서는 절대 rebase 하지 말아야 한다 정도로 정리할 수 있겠습니다.

References