Don’t ever use git pull

Published on
default

어그로성 제목을 써 보았습니다. 하지만 맞는 말 이라고 생각합니다. 만약 습관적으로 git pull하고 있다면, 이제부터는 git pull --rebase 를 하세요!

왜냐고 물으신다면, 그게 더 깔끔하기 때문입니다. 프로그래밍에서 깔끔함은 단순히 보기 좋다를 넘어서서 더 높은 생산성을 보장합니다. 더 높은 생산성을 가진 개발자는 평균적으로 더 높은 연봉을 받습니다. 그러므로 rebase를 사용해서 항상 최신화를 한다면 더 높은 연봉을 받을 수 있다는 놀라운 결론이 나옵니다.

하지만 그냥 이렇게 말해서는 설득력이 없겠죠. 조금 더 자세하게 살펴보도록 합시다.

git pull의 단점

git pull 은 언제 쓸까요? 서로 다른 브랜치의 내용을 하나로 병합하기 위해 사용합니다. 가장 흔한 예는 아래와 같습니다.

누군가가 main 브랜치에 기능을 추가했는데, 내 로컬 브랜치에서 해당 기능이 필요해서 이를 받아오고 싶은 경우, 우리는 git pull origin main 으로 해당 로컬 브랜치에 main 의 코드를 병합할 수 있습니다.

git pullgit fetchgit merge 의 조합이므로, 최종적으로는 git merge 를 실행하게 됩니다.

default

깃은 git merge 를 할때 fast-forwarded 될 수 있는 상황에서는 fast-forward merge를 실행합니다.

다만 현재는 팀원이 이미 main 브랜치에 무언가를 추가한 상황이라 히스토리가 다르므로 git pull origin main 은 merge commit을 남기는 3 way merge를 실행합니다.

두 브랜치가 갈라져 나온 시점의 공통 커밋, 각자 브랜치의 마지막 tip을 비교해서 merge commit을 만든 후, 충돌이 있으면 충돌 부분을 해결하고 병합을 마무리합니다.

그렇게 해서 훌륭하게 브랜치를 병합했습니다. 아무 문제가 없어 보입니다. 그러나.. main 브랜치가 active할 수록 아래 그림과 같은 상황이 일어나게 됩니다.

default

히스토리가 수많은 merge commit으로 가득 차게 되고 그래프들이 난리가 납니다. 깔끔과는 거리가 있죠.

git pull의 장점과 한계

그러나 오해 마세요. git pull 은 분명한 장점이 있습니다. 일단 비파괴적인 방식으로 안전합니다. 브랜치를 병합할 때 서로의 히스토리를 건드리지 않습니다.

또한 각 브랜치의 커밋들을 그대로 남길 수 있어 디버그 할때 정확하게 어느 부분에서 문제가 일어났는지 확인할 수 있다는 장점을 가지고 있죠. 무언가가 꼬였을때 쉽게 시간 여행을 할 수 있습니다. 모든 기록이 남아 있으니까요.

그러나 역설적이게도 모든 기록이 남아 있다는 것은 분명한 단점이기도 합니다. 프로젝트 히스토리가 길어질 수록 의미 없는 merge commit이 섞이게 되어 히스토리를 알아보기 힘들게 만듭니다. 다른 개발자가 파악을 하는 것이 더 힘들어지죠.

git pull의 한계를 해결하는 방법 — rebase

git rebase는 이런 문제를 훌륭하게 해결할 수 있는 방법입니다. 히스토리를 좀 더 보기 편하게, 의미 있게 만들어 줍니다. merge commit을 남기지 않는 것은 물론입니다.

흔히 git rebase 는 다루기 까다로운 옵션이라고 생각되어지곤 합니다. 그러나 사실은 아주 간단합니다. git mergegit rebase 는 둘 다 서로 다른 브랜치의 병합을 위해서 사용합니다. git rebase 가 다른 점은, merge commit을 남기지 않는 다는 것 뿐입니다.

merge commit은 각 브랜치간의 차이점을 비교하고 병합하는 과정에서 나온 부산물입니다. git rebase 가 merge commit을 남기지 않는 이유는 내 커밋들을 기준이 되는 브랜치의 뒤로 베이스를 옮겨 버리기 때문입니다. 정확히 말하자면 커밋들을 새로 써서 옮겨 버립니다.

default

그림으로 보면 이해가 더 잘 가죠? git pull --rebase 명령어는 git fetch 를 실행한 다음 git rebase 를 실행합니다. 이는 git merge 처럼 브랜치간의 병합이라는 결과물은 같지만 git rebase 는 히스토리를 1자, 선형으로 만듭니다. 흔히 말하는 1자 커밋 히스토리입니다.

default

1자 커밋 히스토리의 장점은 굳이 말하지 않아도 쉽게 보입니다. 누가 어떤 기능을 언제 병합하였는지 쉽고 빠르게 확인할 수 있습니다. 히스토리 파악과 디버깅이 더 쉬워질 수 있겠죠.

실제 예시로 이해하기

git 에 대한 글들은 개념만 봐서는 잘 이해가 가지 않을 때가 많습니다. 따라서 실제 usecase를 통해 동일한 행동을 git pullgit pull rebase 를 사용했을 때 차이를 보도록 합시다. 행동의 순서는 다음과 같습니다.

먼저 git pull origin dev 의 경우 입니다.

1. initial commit
2. common commit #1 on dev
3. common commit #2 on dev
4. git checkout -b feature-A
5. git checkout -b feature-B
6. feature A #1 on feature-A // feature-A 브랜치에서 작업
7. feature A #2 on feature-A
8. feature B #1 on feature-B // feature-B 브랜치에서 작업
9. feature B #2 on feature-B
10. feature B#3 on feature-B
11. Pull Request feature B(#1, #2, #3) -> dev
12. git checkout feature-A
13. git pull origin dev
14. feature A #3 on feature-A // 이어서 남은 작업 처리하기
15. git push origin feature-A
16. Pull Request feature A(#1, #2, #3) -> dev
default

실제로 그래프는 다음과 같이 나옵니다. 그러면 git pull --rebase origin dev 의 경우를 봅시다. 13번까지의 행동은 동일하고, 차이는 14번에서의 pull--rebase 밖에 없습니다.

1. initial commit
2. common commit #1 on dev
3. common commit #2 on dev
4. git checkout -b feature-A
5. git checkout -b feature-B
6. feature A #1 on feature-A // feature-A 브랜치에서 작업
7. feature A #2 on feature-A
8. feature B #1 on feature-B // feature-B 브랜치에서 작업
9. feature B #2 on feature-B
10. feature B#3 on feature-B
11. Pull Request feature B(#1, #2, #3) -> dev
12. git checkout feature-A
13. git pull --rebase -i origin dev
14. 인터랙티브 모드에서 다 넘기고 저장 후 컨플릭트 해결
15. git rebase - continue
16. feature A #3 on feature-A // 이어서 남은 작업 처리하기
17. git push - force origin feature-A
// 이미 feature-A 브랜치에 작업들을 푸쉬했었으므로 force push

18. Pull Request feature A(#1, #2, #3) -> dev
default

동일한 행동인데 그래프의 차이가 느껴지시나요? 아래의 비교샷을 보면 느껴지실 겁니다.

참고로 pull request 시 나오는 merge commit 도 bitbucket이나 github의 설정에서 rebase를 사용하는 것으로 없앨 수 있지만, PR의 경우는 일부로 남겨놓는 것을 택하였습니다. 해당 PR의 내용을 확인할 수 있고 기능의 범위를 산정하기 편하다는 이유입니다.

지금은 혼자서 간단한 feature 2개를 작업한 예제지만, 실제 수십명의 개발자와 같이 거대한 프로젝트를 할 경우 둘의 차이는 점점 더 벌어지게 될 것입니다.

개발의 많은 것들은 trade-off 관계입니다. 아무래도 안전함보다는 깔끔함에 손을 들어주고 싶습니다. 이것이 바로 git pull 대신 git pull --rebase 를 써야 할 이유라고 생각합니다.

Rebase 하면 뭔가 망할까봐 무서워요!

혹시 이러한 생각이 든다면, 하나만 생각하시면 됩니다.

공유하는 브랜치에서는 절대 Rebase 하지 않는다.

해당 룰만 지킨다면 rebase를 사용해서 문제가 생길 일은 없다고 장담합니다. 간단히 생각해서 내 커밋들의 베이스를 공유하는 브랜치 뒤로 옮긴다면 아무 문제가 없지만, 남들이 사용하는 커밋들의 베이스를 내 브랜치 뒤로 옮긴다면 난리가 나겠죠.

그러면 앞으로 git pull --rebase 와 함께 행복한 git 사용 하시길 바라며 글을 마치도록 하겠습니다.