git rebase란 무엇이며 왜 사용하는가?
[git rebase란?]
프로젝트를 깃으로 관리한다면, 브랜치 가지치기와 합치는 과정은 필수이다. 그리고 현재 브랜치를 다른 브랜치로 합치는 명령어는 merge
와 rebase
가 있다.
[git rebase와 merge의 차이]
merge
는 두 브랜치를 합치는 가장 쉬운 방법으로, 두 브랜치의 가장 마지막 커밋 두 개 (C3, C4) 를 공통 조상에 합치는 3-way Merge로 새로운 커밋 (C2)를 만들어 낸다.
rebase
는 두 브랜치를 합치는 다른 방법으로, 두 브랜치의 공통 커밋(Common Commit)으로 이동하고 나서, 다른 브랜치의 마지막 커밋(Another Last Commit) 으로 이동한다. 그리고 Another Last Commit 에서부터 현재 브랜치의 Common Commit 부터 현재 브랜치의 마지막 Commit까지 Diff들을 차례로 해결해나간다
merge와 rebase의 차이를 실습으로 살펴보자
이번 실습은
master 브랜치에서 2개의 브랜치 (branch-a, branch-b) 를 분기하고,
master와 분기한 브랜치에서 각각 commit을 2개씩 날린 후,
변경 내역들을 master에 합치는 과정을 진행한다.
각 commit은 hello.txt 파일에 강제로 Merge Conflict을 일으키게 진행했다.
[merge]
현재 상태를 Git 그래프로 보면 다음과 같다.
이제 master 브랜치에서 branch-a 와 branch-b 를 각각 합치고 충돌도 해결하면 위와 같이 2개의 Merege 커밋이 생기는 것을 볼 수 있다
[rebase]
rebase 또한 동일한 실습으로 위와 같은 환경에서 시작한다.
rebase에서는 충돌해결 과정을 자세히 보기 위해 다음과 같이 파일을 수정했다
[그림1]
먼저 git checkout branch-a 로 이동해서, branch-a 에서 git rebase master
명령어를 이용해, master와 공통 조상으로 이동 (init master) 로 이동한 뒤, master의 마지막 커밋인 “commit2 master” 에 대해 rebase를 진행한다.
만약 git bash를 사용한다면 (branch-a | REBASE 1/2)
라는 텍스트를 살펴볼 수 있다.
이 말은 즉, rebase하려는 master의 마지막 커밋인 “commit2 master”와 branch-a의 첫 번째 커밋인 “commit a”의 충돌이 난 것이다.
이를 해결하고 다시 git rebase --continue
를 진행하면
[그림2]
다시 위와 충돌이 나면서 git bash 로 보면 (branch-a | REBASE 2/2)
라는 텍스트를 살펴볼 수 있을 것이다. 이는 branch-a 의 2번째 커밋과 rebase하려는 커밋의 충돌을 해결 중이라는 것이다.
근데 위에서 주의깊게 살펴볼 점은, branch-a 의 두 번째 커밋은 “commit1 a” 라는 내용을 지우고 “commit2 a” 라는 내용을 썼는데, 아까 충돌을 해결하는 과정에서 “commit1 a” 라는 내용을 [그림1]에서 충돌 해결하는 과정에서 다시 살려버렸다는 것이다.
[그림3]
그래서 위와 같이 다시 수정한 다음 git rebase --continue
를 진행하고 rebase를 마무리한다.
[그림4]
위 그림과 같이 master의 마지막 커밋에 이어서 커밋이 날라간 것이 됩니다. 이게 가능한 이유는rebase는 새로운 커밋을 만들기 때문이다.
이제 master 브랜치를 rebase한 branch-a 로 fast-forward하도록 merge한다 (master 브랜치에서 git merge branch-a
)
그 다음 branch-b로 이동해서 git rebase master
를 입력한다.
[그림5]
branch-a 와 마찬가지로 충돌을 해결한다.
[그림6]
최종적으로 공통 커밋에서 시작한 브랜치들을 [그림6] 처럼 이어붙인 커밋으로 히스토리를 관리할 수 있다.
가장 큰 장점은 merge
에 비해 커밋 히스토리를 깔끔하게 관리할 수 있다는 점이다. 커밋들의 인과관계가 하나이기 때문에 직관적으로 보기에 좋다.
하지만 실제로 팀 프로젝트에 git rebase를 다 같이 적용하면서 어려웠던 경험은 다음과 같다.
공통 커밋에서 출발한 이후로 적었다 지웠다를 많이 반복한 코드의 경우, rebase
로 충돌 해결할 때, 이전에 내가 삭제했던 코드가 다시 살아나서 충돌해결하는 경우가 많다는 것이다.
위에서 [그림2]와 [그림3] 을 보면 “commit1 a” 라는 커밋과 “commit2 a”라는 커밋 2개 모두 master 브랜치와 충돌이 나서 해결하는 과정을 보면, 내가 의도한 마지막 코드는 “commit2 a”인데 이전에 rebase 충돌을 해결하는 과정에서 깜빡하고 “commit1 a” 코드를 살려서 충돌 해결을 했기 때문에 이후 REBASE (2/2)
에서 죽은 코드를 살려버린 실수를 했다.
위와 같은 1줄 충돌은 실수를 잡기 쉽지만, 이게 여러 파일을 건드리고 복잡한 로직이 연관된 경우, 내가 작성한 코드에도 불구하고 헷갈릴 수 있다.
실제로 프로젝트에 rebase로 합치자는 규칙을 적용했지만, 충돌 해결 과정에서 실수할 가능성이 컸기 때문에
작은 단위의 PR로 쪼개거나, 수정할 파일을 분리하여 충돌을 최소화하자는 합의가 필요했다.
⚠️이미 공개 저장소에 Push 한 커밋을 Rebase 하지 마라⚠️
git rebase
를 사용하면 내가 커밋했던 기록을 바꾼다
원격 저장소에 push한 브랜치를 다시 다른 브랜치에 rebase를 하게 될 경우, 새로운 커밋으로 바뀌기 때문에 다른 브랜치가 고생하게 된다.
예시로 다음 사례를 만들어봤다.
1. master 에서 커밋 2개를 만들고 원격 저장소에 Push한다.
2. branch-a 가 master 의 HEAD에서 커밋 1개를 만든다.
3. branch-b 가 master 를 rebase하고 커밋 2개를 만든다.
4. master 가 branch-b 를 rebase한다.
- ⚠️이미 원격 저장소에 Push된 commit1 master
와 commit2 and push master
를
branch-b와 합치면서 새로운 커밋으로 또 만든다
commit1 master
와 commit2 and push master
가 다른 커밋으로 바뀌었기 때문에 다시 충돌을 해결해야하는 일이 발생한다. commit master
부터 충돌이 발생했다출처: https://git-scm.com/book/ko/v2/Git-브랜치-Rebase-하기
git rebase
를 충돌이 많이 없을 경우 조심해서 사용한다면 히스토리를 깔끔하게 관리하기 정말 좋은 방법이다. 하지만 실제 프로젝트를 git rebase
로만 관리하려 했지만, 이전에 삭제했던 히스토리를 깔끔하게 관리하다가 코드가 꼬일 경우는 과감하게 포기하고 git merge
를 사용하는 것을 추천한다.