Home [Git] Rebase 기초
Post
취소

[Git] Rebase 기초

한 브랜치에서 다른 브랜치를 합치는 방법으로는 두 가지가 있다. 하나는 Merge 이고 다른 하나는 Rebase 이다. 이 절에서는 Rebase가 무엇인지, 어떻게 사용하는지, 좋은 점은 무엇인지, 어떤 상황에서 사용하고 어떤 상황에서는 사용하지 말아야 하는지 알아 본다.

앞에서 설명한 “Merge 기초” 절에서 살펴본 예제로 다시 돌아가 보자. 두 개의 나누어진 브랜치의 모습을 볼 수 있다.

두 개의 브랜치로 나누어진 커밋 히스토리 두 개의 브랜치로 나누어진 커밋 히스토리

이 두 브랜치를 합치는 가장 쉬운 방법은 앞에서 살펴본 대로 git merge 명령을 사용하는 것이다. 두 브랜치의 마지막 커밋 두 개(C3, C4)와 공통 조상(C2)을 참조하여 3-way Merge로 새로운 커밋을 만들어 낸다.

나뉜 브랜치를 Merge 하기 나뉜 브랜치를 Merge 하기

위와 비슷한 결과를 만드는 다른 방식을 알아보자. C3에서 변경된 사항을 Patch로 만들고 이를 다시 C4에 적용시키는 방법이 있다. Git에서는 이런 방식을 Rebase 라고 한다. git rebase 명령으로 한 브랜치에서 변경된 사항을 다른 브랜치에 적용할 수 있다.

위의 예제는 아래와 같은 명령으로 Rebase 한다.

1
2
3
4
$ git switch experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

위 내용을 설명하자면 일단 두 브랜치가 나뉘기 전의 공통 커밋(C2)으로 이동하고 그 커밋부터 지금 Checkout 한 브랜치가 가리키는 커밋(C4)까지 diff를 차례로 만들어 어딘가에 임시로 저장해 놓는다. Rebase 할 브랜치(experiment)가 합쳐질 브랜치(master)가 가리키는 커밋을 가리키게 하고 아까 저장해 놓았던 변경사항을 차례로 적용한다.

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

그리고 master 브랜치를 C4’ 커밋에 Fast-forward 시킨다.

1
2
$ git switch master
$ git merge experiment

master 브랜치를 C4’ 커밋에 Fast-forward 시키기 master 브랜치를 C4’ 커밋에 Fast-forward 시키기

C4’로 표시된 커밋 내용은 Merge 예제에서 살펴본 C5 커밋 내용과 동일할 것이다. Merge 이든 Rebase 든 합치는 관점에서는 서로 다를 게 없다. Rebase가 좀 더 깔끔한 커밋 히스토리를 만든다는 차이만 있을 뿐이다. Rebase 한 브랜치의 Log를 살펴보면 히스토리가 선형적이다. 일을 병렬로 동시에 진행해도 Rebase 하고 나면 모든 작업이 차례로 수행된 것처럼 보이게 된다.

Rebase는 보통 리모트 브랜치에 커밋을 깔끔하게 적용하고 싶을 때 사용한다. 아마 이렇게 Rebase 하는 리모트 브랜치는 직접 관리하는 것이 아니라 그냥 참여하는 브랜치일 것이다. 메인 프로젝트에 Patch를 보낼 준비가 되면 하는 것이 Rebase 니까 브랜치에서 하던 일을 완전히 마치고 origin/master로 Rebase 한다. 이렇게 Rebase 하고 나면 프로젝트 관리자는 어떠한 통합작업도 할 필요가 없게 된다. 그저 master 브랜치를 Fast-forward 시키면 된다.

Rebase를 하든지, Merge를 하든지 최종 결과물은 같고 커밋 히스토리만 다르다는 점이 중요하다.

Rebase의 경우 브랜치의 변경사항을 순서대로 다른 브랜치에 적용하면서 합치고, Merge의 경우 두 브랜치의 최종 결과만을 가지고 합친다.

Rebase 활용


Rebase는 단순히 브랜치를 합치는 것만 아니라 다른 용도로도 사용할 수 있다.

특정 토픽 브랜치에서 갈라져 나온 토픽 브랜치가 있는 히스토리를 생각해보자. server 브랜치를 만들어서 서버 기능을 추가하고 그 브랜치에서 다시 client 브랜치를 만들어 클라이언트 기능을 추가한다. 마지막으로 server 브랜치로 돌아가서 몇 가지 기능을 더 추가한다.

특정 토픽 브랜치(server의 C3 커밋)에서 갈라져 나온 토픽 브랜치(client의 C8) 특정 토픽 브랜치(server의 C3 커밋)에서 갈라져 나온 토픽 브랜치(client의 C8)

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

1
$ git rebase --onto master server client

이 명령은 master 브랜치부터 server 브랜치와 client 브랜치의 공통 조상(C2)까지의 커밋을 client 브랜치에서 없애고 싶을 때 사용한다. client 브랜치에서만 변경된 패치를 만들어 master 브랜치에서 client 브랜치를 기반으로 새로 만들어 적용한다. 조금 복잡하긴 해도 꽤 쓸모 있다.

특정 토픽 브랜치(C3)에서 갈라져 나온 토픽 브랜치(C8, C9)를 Rebase 하기 특정 토픽 브랜치(C3)에서 갈라져 나온 토픽 브랜치(C8, C9)를 Rebase 하기

이제 master 브랜치로 돌아가서 client 브랜치 위치로 Fast-forward 시킨다.

1
2
$ git switch master
$ git merge client

master 브랜치를 client 브랜치 위치로 이동 master 브랜치를 client 브랜치 위치로 이동

server 브랜치에서 작업이 다 끝나면 git rebase <basebranch-name> <topicbranch-name> 라는 명령으로 바로 server 브랜치를 master 브랜치로 Rebase 할 수 있다. 이 명령은 토픽 브랜치(server)를 Checkout 하고 베이스 브랜치(master)에 Rebase 한다.

1
$ git rebase master server

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

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

이제 master 브랜치를 server 브랜치 위치로 Fast-forward 시킨다.

1
2
$ git switch master
$ git merge server

모든 것이 master 브랜치에 통합됐기 때문에 더 이상 필요하지 않은 client, server 브랜치는 삭제해도 된다.

1
2
$ git branch -d client
$ git branch -d server

최종 커밋 히스토리 최종 커밋 히스토리

브랜치를 삭제해도 커밋 히스토리에 위와 같이 같이 여전히 남아 있는 것을 확인할 수 있다.

Rebase의 위험성


Rebase는 장점이 많은 기능이지만 단점이 없는 것은 아니니 조심해야 한다. 그 주의사항은 아래 한 문장으로 표현할 수 있다.

이미 공개 저장소에 Push 한 커밋을 Rebase하지 말라

이 지침만 지키면 Rebase를 하는 데 문제 될 것은 없을 것이다. 다만, 본인 부주의로 인해 이 주의사항을 지키지 않았을 경우 함께 협업하는 사람들로부터 수많은 삿대질과 욕을 배불리 먹어 장수하게 되는 기적이 올 것이다.

Rebase는 기존 커밋을 그대로 사용하는 것이 아니라 내용은 같지만 새로운 커밋을 만든다. 새 커밋을 서버에 Push 하고 동료 중 누군가가 그 커밋을 Pull 해서 작업을 한다고 하자. 그런데 그 커밋을 git rebase 로 바꿔서 Push 해버리면 동료가 다시 Push 했을 때 동료는 다시 Merge 해야 한다. 그리고 동료가 Merge 한 내용을 Pull 하면 내 코드는 엉망진창이 된다.

이미 저장소에 Push 한 커밋을 Rebase 하면 어떤 결과가 초래되는지 예제를 통해 알아보자. 중앙 저장소에서 Clone 하고 일부 수정을 하면 커밋 히스토리는 아래와 같이 된다.

리모트 저장소를 Clone 하고 일부 수정함 리모트 저장소를 Clone 하고 일부 수정함

이제 팀원 중 누군가 커밋, Merge 하고 나서 서버에 Push 한다. 이 리모트 브랜치를 Fetch, Merge 하면 히스토리는 아래와 같이 된다.

리모트 저장소를 Fetch 한 후 Merge 함 리모트 저장소를 Fetch 한 후 Merge 함

그런데 Push 했던 팀원은 Merge 한 일을 되돌리고 다시 Rebase 한다. 서버의 히스토리를 새로 덮어씌우려면 git push 명령에 --force 혹은 -f 옵션을 사용해야 한다. 이후 저장소에서 Fetch 하고 나면 아래 그림과 같은 상태가 된다.

한 팀원이 다른 팀원이 의존하는 커밋을 없애고 Rebase 한 커밋을 다시 Push 함 한 팀원이 다른 팀원이 의존하는 커밋을 없애고 Rebase 한 커밋을 다시 Push 함

이렇게 되면 히스토리가 짬뽕이 된다. git pull로 서버의 내용을 가져와서 Merge 하면 같은 내용의 수정사항을 포함한 Merge 커밋이 아래와 같이 만들어진다.

같은 Merge를 다시 Merge하여 중복 커밋 발생 같은 Merge를 다시 Merge하여 중복 커밋 발생

git log로 히스토리를 확인해보면 저자, 커밋 날짜, 메시지가 완전히 동일한 커밋 두 개(C4, C4’)가 있다. 이렇게 되면 혼란스럽다. C4와 C6는 포함되지 말았어야 할 커밋이다. 애초에 서버로 데이터를 보내기 전에 Rebase로 커밋을 선형으로 정리했어야 했다.

Rebase 한 것을 다시 Rebase 하기


만약 이러한 상황에 처했을 때 유용한 Git 기능이 하나 있다. 어떤 팀원이 강제로 내가 한 일을 덮어썼다고 하자. 그러면 내가 했던 일이 무엇이고 덮어쓴 내용이 무엇인지 알아내야 한다.

커밋 SHA-1 체크섬 외에도 Git은 커밋에 Patch 할 내용으로 SHA-1 체크섬을 한 번 더 구한다. 이 값은 “patch-id” 라고 한다.

덮어쓴 커밋을 받아서 그 커밋을 기준으로 Rebase 할 때 Git은 원래 누가 작성한 코드인지 잘 찾아 낸다. 그래서 Patch가 원래대로 잘 적용된다.

예를 들어 앞서 살펴본 예제 중 한 팀원이 다른 팀원이 의존하는 커밋을 없애고 Rebase 한 커밋을 다시 Push 한 상황에서 Merge 하는 대신 git rebase teamone/master 명령을 실행하면 Git은 아래와 같은 작업을 한다.

  • 현재 브랜치에만 포함된 커밋을 결정한다. (C2, C3, C4, C6, C7)
  • Merge 커밋이 아닌 것을 결정한다. (C2, C3, C4)
  • 이 중 Merge 할 브랜치에 덮어쓰이지 않은 커밋을 결정한다. (C2, C3)
  • 결정한 커밋을 teamone/master 브랜치에 적용한다.

강제로 덮어쓴 브랜치에 Rebase 하기 강제로 덮어쓴 브랜치에 Rebase 하기

다시 Merge 한 결과는 엉망이였지만, Rebase 한 결과는 선형으로 깔끔하게 정리된 히스토리를 볼 수 있게 된다.

동료가 생성했던 C4와 C4’ 커밋 내용이 완전히 같을 때만 이렇게 동작된다. 커밋 내용이 아예 다르거나 비슷하다면 커밋이 두 개 생긴다(같은 내용이 두 번 커밋될 수 있기 때문에 깔끔하지 않다).

git pull 명령을 실행할 때 옵션을 붙여서 git pull --rebase 로 Rebase 할 수도 있다. 물론 git fetch 와 git rebase teamone/master 이 두 명령을 직접 순서대로 실행해도 결과는 같다.

git pull 명령을 실행할 때 기본적으로 --rebase 옵션이 적용되도록 pull.rebase 설정을 추가할 수 있다. git config --global pull.rebase true 명령으로 추가한다.

Push 하기 전에 정리하려고 Rebase 하는 것은 괜찮다. 또 절대 공개하지 않고 혼자 Rebase 하는 경우도 괜찮다. 하지만, 이미 공개하여 사람들이 사용하는 커밋을 Rebase 하면 틀림없이 문제가 생긴다.

나중에 후회하지 말고 git pull --rebase 로 문제를 미리 방지할 수 있다는 것을 같이 작업하는 동료와 모두 함께 공유하기 바란다.

Rebase vs. Merge


Merge와 Rebase가 어떠한 기능을 하는지 여러 예제를 통해 간단히 살펴보았다. 지금쯤 이런 의문이 들 것으로 생각한다.

“둘 중 무엇을 쓰는 게 좋지?” 이 질문에 대한 답을 찾기 전에 히스토리의 의미에 대해서 잠깐 다시 생각해보자.

히스토리를 보는 관점 중에 하나는 작업한 내용의 기록으로 보는 것이 있다. 작업한 내용을 기록한 문서이고, 각 기록은 각각의 의미를 가지며, 변경할 수 없다. 이런 관점에서 커밋 히스토리를 변경한다는 것은 역사를 부정하는 꼴이 된다. 언제 무슨 일이 있었는지 기록에 대해 거짓말을 하게 되는 것이다. 이렇게 했을 때 지저분하게 수많은 Merge 커밋이 히스토리에 남게 되면 문제가 없을까? 역사는 후세를 위해 기록하고 보존해야 한다.

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

Merge 나 Rebase 중 무엇이 나은가에 관한 질문은 다시 생각해봐도 답이 그리 간단치 않다. Git은 매우 강력한 도구이고 기능이 많아 히스토리를 잘 쌓을 수 있지만, 모든 팀과 모든 이가 처한 상황은 모두 다르다. 예제를 통해 Merge나 Rebase가 무엇이고 어떤 의미인지 배웠다. 이 둘을 어떻게 사용할지는 각자의 상황과 판단에 달렸다.

일반적인 해답을 굳이 드리자면 로컬 브랜치에서 작업할 때는 히스토리를 정리하기 위해서 Rebase 할 수도 있지만, 리모트 등 어딘가에 Push로 내보낸 커밋에 대해서는 절대 Rebase 하지 말아야 한다는 것이다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

[Git] 리모트 브랜치

[Git] Centralized, Integration-Manager and Benevolent dictator workflow