rebase를 처음으로 쓰게되어 공부하는 글… 초짜라 틀린 내용 있을 수 있음
프로젝트에서 git pull origin <branch> 명령어가 아닌 git rebase <branch> 를 사용하기로 했다. rebase는 처음 사용해보는거라 git 대참사를 일으키는 것을 방지하고자 프로젝트 전 미리 알아가려 한다.
우선 정확히 짚고 넘어가자면 pull을 사용하지 않는게 아니라, 그 안에 포함된 merge를 사용하지 않는 것이다.
브랜치 병합에는 크게 2가지의 전략이 있는데, 바로 merge와 rebase이다. git pull명령어는 git fetch + git merge 의 조합이라고 한다. git fetch는 원격 리포지토리를 ‘가져오는’ 역할이고, merge는 fetch를 통해 가져온 변경사항을 로컬 리포지토리에 ‘병합’하는 것이다. 우리 프로젝트에서는 fetch + merge를 사용하지 않고, fetch + rebase를 사용함으로써 커밋 히스토리를 깔끔하게 정리하기로 했다. 인텔리제이와 같은 IDE에서는 자동으로 Auto Fetch를 해주므로 우리는 그냥 rebase만 해주면 된다.
결론적으로 우리 프로젝트에서 main 브랜치의 변경사항을 작업 브랜치로 반영하기 위해서 아래와 같은 명령어를 사용하면 된다.
git pull --rebase origin <main branch명>
위 명령어는 pull 옵션으로 rebase를 수행하는 명령어로, git fetch origin <main branch명> + git rebase origin/<main branch명> 와 같다.
명령어만 외워서 사용하다간 나중에 큰일날 수도 있으므로 정확히 원리를 알아보고 사용해보자. 덧붙여 rebase라고 하면 ‘커밋마다 충돌이 나면 해결해주어야한다’가 대표적인 단점이라고 알려져있는데, 왜 그런지는 동작원리를 보면 이해할 수 있다.
💡이 글에서 사용하는 main branch명은 main이 아닌 develop이다.
Rebase 동작원리
rebase는 A 브랜치(현재의 head) 커밋을 B 브랜치의 끝으로 이동시켜 히스토리를 재정렬하고, merge는 A와 B를 병합하여 새로운 커밋을 만들어낸다(Merge commit).
develop 브랜치에서 파생된 feature 브랜치가 있다고 가정했다. commit 상태는 아래와 같다. 피그마로 급조해봄…
아래와 같이 B → C의 diff(변경사항)를 X로, C → D의 diff를 Y라고 정의해보았다. 현재의 HEAD는 commit D이다. 이 상태에서 git pull --rebase origin develop 명령어를 입력했을 경우 동작과정을 알아보자.
1. 기존 커밋 C와 D가 사라지고, 커밋들의 diff인 X와 Y가 임시 Stack 저장소에 저장된다. HEAD는 develop의 최신 커밋인 F로 이동했다.
2. 커밋 F에 diff X가 반영되어 새로운 커밋 C’가 만들어진다. HEAD가 C’로 옮겨간다.
3. C’에 diff Y가 반영되어 새로운 커밋 D’가 만들어진다. HEAD가 D’로 옮겨간다.
4. rebase가 완료되었다. 최종적인 모습은 아래와 같다.
초기의 모습과 비교해보면, C와 D는 rebase 후 완전히 다른 commit인 C'와 D'가 된 것을 확인할 수 있다. commit 중간에 E와 F라는 커밋이 끼어들었기 때문에 당연한 결과이다. 실제로 깃허브에서 이미 push한 commit을 rebase 후 다시 push하면 해시값이 다른 것을 확인할 수 있다.
Rebase시 주의할 점
1. 순차적인 충돌
이것이 바로 <rebase 시 많은 충돌이 난다>라고 알려지는 이유이다. 위의 과정에서 feature의 커밋 C나 D에서 develop의 커밋 E 혹은 F와 동일한 파일을 건드렸을 경우 diff X나 Y를 적용할 때(2, 3단계 때) 충돌이 난다. 최악의 경우 diff이 적용될 때마다 충돌이 날 가능성이 있고, 이것들을 하나하나 해결해주어야한다.
개인적으로 이게 rebase의 가장 큰 단점이라고 생각한다…. 협업할 때 나와 같은 파일을 건드린 다른 개발자의 코드가 develop에 merge 되면 아… 충돌 엄청 나겠구나 하고 마음의 준비를 미리 해야한다.
충돌을 해결하려면 아래와 같은 과정을 따르면 된다. 요즘은 인텔리제이 같은 IDE에서 명령어 없이 간단하게 해결가능하지만 명령어는 알아두자.
git status # 1. 어디서 충돌났는지 확인하기
# 2. 충돌 수동으로 해결하기
git add/rm <충돌난 파일> # 3. 충돌 해결해서 add 혹은 rm
git rebase —continue # 4. rebase 계속 진행
# 충돌 모두 해결할 때까지 1-4 반복
참고로 충돌 하나하나 해결하다가 뭔가 잘못건드린 것 같다 싶으면 git rebase —abort 명령어로 rebase 하기 전으로 돌아갈 수도 있다.
2. force push
또한 위에서 언급했듯 예시의 C와 C’는 diff은 같지만, rebase 후 다른 커밋 취급받는다. rebase시 재정렬되는 커밋들은 diff만 같은 서로 다른 커밋들이라는 것을 알아두어야한다(확인해보면 커밋 해시값이 다름).
만약 이미 origin에 C, D가 push 된 상태라면, rebase 후 origin에 다시 push할 때 일반 push가 아닌 force push를 해주어야한다.
git push -f origin [브랜치명]
Rebase와 Merge의 차이점
깃을 사용하면서 pull 명령어를 입력하면 아래와 같은 메시지와 마주칠 때가 있다.
... 생략
hint: git config pull.rebase false # merge
hint: git config pull.rebase true # rebase
hint: git config pull.ff only # fast-forward only
... 생략
fatal: Need to specify how to reconcile divergent branches.
이 메시지는 깃허브 병합 전략을 선택하라는 메시지이다. 병합 전략으로는 아래 나온 것과 같이 크게 3가지가 있는데 나는 여기서 merge와 rebase의 차이점에 대해서 알아보고자 한다.
위와 같은 상태에서 rebase 시 결과는 왼쪽, merge는 오른쪽이다.
그림을 보면 merge 실행시 rebase 처럼 뒤로 F에 C와 D의 diff가 적용되는 것이 아니라, 기존의 HEAD였던 D에 새로운 merge commit이 생성되는 것을 확인할 수 있다.
merge는 rebase처럼 최악의 경우 내가 한 commit마다 충돌을 해결할 필요는 없고, merge commit이 생성되는 1번만 충돌을 해결해주면 된다. 그러나 불필요한 merge 커밋이 생성된다는 단점이 있다. 직접 이름을 지정해주지 않을 시 origin의 메인 브랜치를 pull할 때마다 아래와 같이 커밋기록이 생성된다.
그리고 rebase의 장점은 커밋 히스토리를 선형적으로 깔끔하게 관리할 수 있다는 것이다. 아래 사진에서 왼쪽은 pull 전략으로 rebase(메인 브랜치에 push 할땐 squash and merge 함)를, 오른쪽은 merge를 선택한 프로젝트이다.
merge는 커밋이 분기 형태로, rebase는 직선 형태로 남는다. 그렇기 때문에 브랜치가 많은 프로젝트일 수록 rebase로 히스토리를 관리하는 것이 훨씬 깔끔하다.
🤔 번외) Squash and merge 한 이유
PR의 merge 옵션으로는 아래 사진과 같이 3가지가 있다. 우리는 여기서 2번째인 Sqaush and merge를 사용하기로 했다.
사용한 이유는 커밋 히스토리를 깔끔하게 정리할 수 있어서이다. 서버 레포 병합 전략으로 rebase를 선택한 이유가 커밋 히스토리를 깔끔하게 관리하기 위함이였으므로, Merge pull request 전략도 Squash and merge를 선택해서 더 깔끔하게 관리하고자 했다.
저 옵션을 클릭하면 아래 사진의 왼쪽과 같이 뜨는데 안의 커밋 내용도 수정할 수 있다. Confirm squash and merge를 클릭한 뒤 커밋 로그로 들어가보면 오른쪽 같이 나온다. 여러 개의 커밋이 하나의 커밋으로 병합되었다.
그러니까 분기된 브랜치 내용을 develop에 병합하는 것을 마치 한 커밋으로 남길 수 있게 되는 것이다. 마치 1 merge 1 commit으로 작동하게 되는 듯하다. 잔디에도 1 커밋으로 반영되어서 한 브랜치에서 아무리 많은 작업을 해도 잔디색깔은 연하게 나온다.......🥲
Rebase 장단점 정리
아무래도 협업을 하다보면 main 브랜치의 변경사항을 땡겨올 일이 많은데, merge를 하면 그 때마다 Merge commit이 생성될 것이다. 뭔가 필요없는 commit이 생기는 것 같아 거슬리고, 팀의 commit convention과도 맞지 않을 확률이 크다(물론 바꿔줄 수 있지만 일일히 해주기 귀찮음). 또한 merge는 브랜치가 늘어날수록 분기가 늘어나 커밋그래프가 복잡해서 한눈에 파악하기 힘들어질 수 있다. 반면 rebase는 위의 실행결과에서 볼 수 있다시피 커밋 히스토리를 깔끔하게 관리할 수 있다.
그러나 위의 [Rebase 동작원리] 에서 확인할 수 있듯이 rebase 수행 후 기존의 커밋들은 새로운 커밋이다. 만약 2명의 개발자가 같은 브랜치에서 작업한다면 remote의 커밋과 local의 커밋이 불일치하는 문제가 발생한다. 이 경우 git push -f 명령어를 통해 강제 push를 진행해야하는데, 이 경우 해결하기 힘든 충돌로 git이 꼬일 수 있다. 따라서 rebase를 사용하는 경우 무조건 서로 다른 브랜치에서 작업하도록 해야한다.
그러니까 요약하자면 아래와 같다.
장점
- 불필요한 Merge commit이 발생x
- Commit history를 선형적으로 깔끔하게 관리할 수 있다.
단점
- 같은 브랜치를 공유하는 경우 문제가 생길 수 있다. → rebase 사용 시 개발자들은 서로 다른 브랜치를 사용해야한다.
- 커밋마다 충돌이 나면 일일히 다 해결해야한다.
결론
- rebase는 선형적으로 커밋 히스토리를 깔끔하게 관리하고 싶을 때 사용하면 될 것 같다. 더불어 squash and merge도. 기존의 commit들을 남기고 싶으면 그냥 rebase and merge를 써도 될 것 같다.
- 뭐든 프로젝트의 특성과 팀의 규칙에 맞게 하기. rebase, merge 중 정답은 없는 것 같다. 우리는 커밋히스토리를 깔끔하게 관리하려는 의도로 rebase + squash and merge를 채택한 것이다. 이러한 전략은 팀과 맞추면 될 듯하다. 팀원들이랑 잘 의논해보자.
- 언젠간 merge 전략 중 하나인 fast-forward 방식에 대해서도 톺아보기.
- 이 글은 프로젝트 시작할 때 써서 프로젝트 마치고 다시 퇴고하는데 rebase도 쓰다보면 익숙해진다..! 처음엔 충돌 해결하기 어렵다해서 겁 먹었는데 직접 써보니 이젠 능숙하게 쓸 수 있다. 아직 안 써봤다면 한 번쯤은 직접 써보면서 익혀보는 것도 좋을 듯.
'톺아보기' 카테고리의 다른 글
[JPA] JPA 톺아보기 (3) | 2024.09.04 |
---|