Codesigner

[Git] Git Advanced - Rebase overview 본문

Git /git

[Git] Git Advanced - Rebase overview

eunsukimme 2019. 4. 16. 00:10

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

 

 

 

Rebase 기초

 

다음과 같은 브랜치의 모습을 가정하자

 

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

 

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

 

<그림 2> master 브랜치와 experiment 브랜치를 merge하기     (출처: Pro Git Book)

 

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

C:\Users\glafu\Desktop\rebase_test>git checkout experiment
Switched to branch 'experiment'

C:\Users\glafu\Desktop\rebase_test>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)가 가리키는 커밋을 가리키게 하고, 아까 저장해 놓았던 변경사항들을 차례대로 적용시킨다. 아래 그림은 이러한 과정을 나타내고 있다

 

<그림 3> C3의 변경사항을 C4에 적용하는 rebase 과정 (출처: Pro Git Book)

 

그리고 나서 master 브랜치를 Fast-forward 시킨다

 

<그림 4> master 브랜치를 Fast-forward 시키기 (출처: Pro Git Book)

 

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

 

rebase는 보통 리모트 브랜치에서 커밋을 깔끔하게 적용하고 싶을 때 사용한다. 즉 로컬 브랜치에서 작업한 내용을 origin/master 로 rebase 하여 리모트의 master 브랜치를 Fast-forward 시키면 된다. 아마 이렇게 rebase 하는 모든 브랜치는 직접 작업하는 것이 아니라 그냥 참여하는 브랜치일 것이다

 

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

 

 

 

Rebase 활용

 

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

 

<그림 5> 다른 브랜치(server)에서 또 갈라져 나온 브랜치(client) (출처: Pro Git Book)

 

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

git rebase --onto master server client

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

 

<그림 6> 다른 브랜치에서 갈라져 나온 client 브랜치를 master 브랜치에 rebase하기 (출처: Pro Git Book)

 

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

git checkout master
git merge client

<그림 7> master 브랜치를 Fast-forward 시키기 (출처: Pro Git Book)

 

자, 이제 server 브랜치의 작업이 완료되었다고 하자. 우리는 다시 server 브랜치로 checkout 할 필요 없이 다음 명령으로 basebranch 에 topicbranch를 rebase 할 수 있다

git rebase [basebranch] [topicbranch]

즉, 여기서는 basebranch가 master이고 topicbranch가 server브랜치이다. 다음 명령은 topicbranch(server)로 checkout 하고 basebranch(master)로 rebase 한다

git rebase master server

위 명령의 결과는 다음과 같다

 

<그림 8> master 브랜치에 server 브랜치의 수정사항을 적용 (출처: Pro Git Book)

 

그리고 나서 master 브랜치를 다시 Fast-forward 시킨다

git checkout master
git merge server

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

git branch -d client
git branch -d server

<그림 9> 최종 커밋 히스토리 (출처: Pro Git Book)

 

 

 

 

Rebase의 위험성

 

Rebase는 브랜치를 병합할 때 merge와는 달리 커밋 이력이 존재하여 눈으로 변경 내용을 좀 더 명확하게 확인할 수 있고, 선형적인 히스토리를 만들어 준다는 장점이 있다. 하지만, 단점이 존재하지 않는 것은 아니라 주의해야 한다. 그 주의사항은 아래 한 문장으로 표현할 수 있다.

 

이미 공개 저장소에 push 한 커밋을 rebase 하지 마라

 

이 것만 지키면 rebase를 하는 데 문제 될 게 없다. 하지만, 지키지 않는다면 공개 저장소를 공유하는 협업자들에게 무진장 욕을 얻어먹을 것이다...

 

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

 

<그림 10> 저장소를 clone 하고 일부 수정한 상태 (출처: Pro Git Book)

 

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

 

<그림 11> 팀원이 작업한 후 push 한 remote 를 pull 받은 상태 (출처: Pro Git Book)

그런데 push 했던 팀원이 merge 했던 커밋을  되돌리고 다시 rebase 하려고 한다. 서버의 히스토리를 덮어 씌우려고 git push --force 명령을 사용하여 리모트에 push 하였다고 가정하면 커밋 히스토리는 다음과 같게 된다(참고로 지금 팀원의 행동이 앞에서 말한 주의사항을 지키지 않은 것이다!)

 

<그림 12> 한 팀원이 우리가 의존하는 커밋을 없애고 rebase 한 후 push 함 (출처: Pro Git Book)

 

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

 

<그림 13> 같은 merge를 다시 반복한다 (출처: Pro Git Book)

 

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

 

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

 

 

 

Rebase vs Merge

 

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

 

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

 

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

 

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

 

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

 

 

Comments