#git, #github, #merge

Git Merge


devfeature branch로 merge 관련된 여러 개념을 이해해봅시다!

dev : base branch
feature : dev branch에서 나눠진 하위 branch


오늘 생각해볼 내용은 이렇습니다.

  • merge

    • fast-forward merge
    • non fast-forward merge

  • Github에서의 여러 가지의 merge

    • merge
    • rebase and merge
    • squash and merge

fast-forward merge

image

image

feature branch를 dev branch로 병합할 때, dev branch에 새로운 작업 내역이 없다면 merge commit 없이 HEAD만 변경할 수 있습니다.
이를 fast-forward(빨리 감기) merge라고 부릅니다.

  • 현재 branch(feature)의 HEAD를 대상 branch(dev)의 HEAD로 옮깁니다.
  • 이는 github에서 지원하지 않습니다. - 참고

    • 즉, github에서 merge를 진행할 경우 fast-forward 옵션은 활용할 수 없습니다.

image

fast-forward merge의 결과물은 이와 같습니다.

정리하자면, 상위 branch에 변경사항이 없을 때만 fast-forward merge가 가능합니다.
하지만 github에서 fast-forward option을 지원하지 않습니다.



non fast-forward merge

  • 만약 merge하려는 상위 branch에 변경사항이 있다면?
  • 이런 경우에는 양쪽의 변경을 가져오는 merge commit이 있어야 합니다.

image

feature branch에서 작업을 완료하고 dev branch로 합치려고 하는데, dev branch에 다른 협업자들이 작업한 결과물이 새로 commit 되어 dev branch가 달라졌다면?

  • feature branch를 dev에 merge하기 위해서는 dev branch 내의 변경 내용과 feature branch 내의 변경 내용을 하나로 통합할 필요가 있습니다.
  • 따라서 양쪽의 변경을 가져온 merge commit을 실행하게 됩니다.

image

이를 non fast-forward merge라고 합니다.
non fast-forward merge를 하게 되면 branch가 그대로 남기 때문에, 해당 branch로 작업한 내용들을 확인하기에 유용합니다.



Github에서의 여러가지 merge

저는 보통 github 페이지에서 쉽고 간단하게 버튼 하나로 merge를 진행합니다.
github 페이지에서 할 수 있는 merge는 세 가지가 존재합니다.

image

  1. create a merge commit
  2. squash and merge
  3. rebase and merge

위와 같이 존재하는데, merge 버튼 옆에 있는 토글을 열면 merge option 들을 확인할 수 있습니다.
기본으로는 create a merge commit으로 설정되어 있습니다.


1️⃣ create a merge commit

앞에서 설명한 non fast-forward 입니다.
base branch로 merge를 진행할 때, merge commit을 통해 코드를 합칩니다.

image

merge 버튼을 누르면, merge commit을 작성하는 칸이 나옵니다.


2️⃣ squash and merge

다음은 squash and merge인데,
squash의 사전적 의미는 과일 같은 것을 쭈욱 으깨는 것을 말합니다.
과일 대신.. commit을 생각해봅시다!

dev branch로 merge하는 상황을 가정했을 때,

image

위와 같이 merge되는 것을 squash merge라고 합니다. feature branch의 x, y, z 커밋을 하나로 쭈욱 합쳐서 dev branch로 병합될 수 있도록 해줍니다.

  • x, y, z를 합쳐서 새로운 commit을 만듭니다.
  • 이 새로운 commit은 dev branch의 HEAD를 parent로 갖습니다.

잠깐 ! squash and merge는 어떨 때 유용할까요?

저는 진행하고 있는 프로젝트에서 대댓글 기능 구현 역할을 맡았습니다.
저는 프론트로서 대댓글 기능 구현을 하기 위해, 여러 commit을 진행할 것 입니다.

 UI 만들어야지 ..
 mocking 해야지 ..
 함수 연결해야 하지 ..
 어 스타일.. 맘에 안 드네 고쳐서 commit 해야지 ..

위와 같이, 한 기능당 여러 commit이 발생할 수 있는데,
이 commit들이 dev branch commit 내역에 그대로 남는다면, 기능 별로 commit log를 확인하는 데에 있어 불편함을 겪게 되고, 미관상으로도 좋지 않습니다.

sqaush and merge 기능을 활용하면, 제가 feature/대댓글 branch에서 작업한 내용들이 하나의 커밋으로 합쳐지게 됩니다.

image

예를 들면, 기능(feature) 하나로 commit이 쭈욱 합쳐집니다. (dev branch commit log가 아주 예뻐지겠죠 ?!)

image

제가 feature branch에서 진행한 commit들은 합쳐진 commit 하나 속의 details로 남겨집니다.



3️⃣ rebase and merge

rebase and merge를 알기 전에, 우선 rebase가 뭔지 알아야합니다.

rebase

두 개의 공통 base를 가진 branch에서 (A, B),
한 branch(A)의 base를 다른 branch(B)의 최신 commit으로 base를 옮기는 작업을 말합니다.

이름에서 알 수 있듯이 re(다시) base를 설정하는 것 입니다.


예시로 이해해봅시다!

image

현재 저는 feature branch에서 작업을 진행하고 있습니다.
(feature branch는 dev branch로부터 파생된 branch이며 b commit까지 쌓인 이후, 저는 feature branch를 생성했습니다.)

feature branch에서 x, y commit을 하며 열심히 작업하고 있는 도중,
저의 동료 개발자가 c, d라는 작업을 완료해서 dev에 merge를 했습니다.

dev branch는 그 전과 작업 내용(feature branch를 생성하던 시기)이 달라졌기 때문에,
제가 작업을 완료하고 dev branch로 merge할 시기에 충돌이 날 가능성이 있습니다. 이때 rebase를 활용할 수 있습니다.

image

rebase를 활용하면 제 branch(feature)의 base commit은 b지만.
dev의 최신 작업 내용인 d로 base commit을 변경할 수 있습니다.
즉, 전과 달리 위와 같은 그림이 됩니다.


base를 바꾼다는 것은 알겠는데, rebase를 활용하면 얻는 이점은 무엇이 있을까요?

1. dev branch의 최신 변경사항을 즉각 반영할 수 있습니다. (즉, 최신 dev의 최신 commit을 반영하면서 작업을 해야할 때 좋습니다.)

예를 들어, 저는 대댓글 기능을 작업하고 있고, 동료 개발자는 댓글 부분의 오류를 수정하고 있다고 가정해봅시다.
대댓글을 작업하는 동시에, 동료 개발자가 dev branch로 merge 완료된 작업을 rebase를 통해 버그가 수정된 작업본을 바로 이어받아 진행할 수 있습니다.

2. merge commit 이력을 남기지 않아, 깔끔한 commit history를 유지할 수 있습니다.

만약 동료 개발자가 한 작업(dev에 merge됨)이 저의 코드와 충돌이 발생한다면 어떻게 할까요?
rebase를 활용하지 않는다면, 충돌을 해결한 merge commit을 push 해야 해서 commit history가 더럽혀질 수 있습니다.
하지만 rebase를 활용하면, 제 commit들이 마치 충돌이 발생하지 않았던 것처럼 동료 개발자 commit 뒤에 위치하게 되므로 깔끔하게 commit history를 유지할 수 있습니다.


어.. 그런데 rebase를 했더니 계속 같은 충돌을 해결하래요 ㅠㅠ 왜이래요 이거!

제가 처음 rebase를 알았을 때, 계속 충돌이 나서 저는 ‘내가 뭘 잘못 만졌구나..’라고만 생각했습니다.
하지만 이는 rebase의 과정을 생각해보면 이해할 수 있습니다.
image

위 그림에서 다시 생각해봅시다.

만약 동료 개발자가 dev로 merge한 PR의 작업(c, d)들이 나의 코드와 충돌이 나는 작업이었다면?
rebase를 딱 시작했을 때, 제 commit은 x, y가 존재합니다.
제 commit인 x, y는 모두 b를 기준으로 작업했기 때문에 x, y 둘 다 d의 뒤로 보내주기 위해서는 x, y commit 둘 다 충돌을 해결해주어야 합니다.

image

이런 이유로 계속 충돌을 해결하라는 메시지가 뜨는 것 입니다.
이는 rebase의 단점으로 꼽히는데요. commit 마다 충돌 처리를 해주어야 하기 때문입니다.


다시 rebase and merge로 돌아와봅시다.

image image

rebase and merge를 진행하면 위와 같은 그림이 그려질 것 입니다.
x commit의 base를 b에서 d로 변경하며 merge가 됩니다.
rebase and merge를 활용하면 merge commit 없이 merge를 진행할 수 있고,
squash merge와는 다르게 하나의 커밋으로 합쳐지지 않고 PR의 commit 하나하나 살아서 dev branch로 merge 됩니다.



다양한 merge, 어떻게 활용하면 좋을까요?

저희 팀에서는 rebase를 한 후, squash and merge하는 전략을 채택하고 있습니다.
만약 slack에서 동료 개발자 혹은 저의 PR이 merge되었다는 알림이 오면 rebase를 진행합니다.

git fetch origin dev
git rebase origin/dev

위와 같이 입력하고 rebase를 진행하며 충돌이 있다면, 충돌을 하나하나 해결해줍니다.
(만약 IDE에 현재 작업 내용이 있다면 stash를 활용합니다.)

rebase를 활용하면, 최신 작업 내용을 가져올 수 있고, 충돌 해결을 위한 commit이 없어져서 활용하고 있습니다.
이후, squash and merge를 진행하는데, dev에 기능 별로 commit이 쌓이기 때문에 commit log가 깔끔해지는 이유로 사용하고 있습니다.



이 방식은 주의해야할 점이 있습니다.

하지만 이 방식에는 주의해야할 점이 있습니다.
만약 main을 서비스용 branch로 사용하고,
dev를 개발용 branch로 사용한다고 하면
dev branch를 main branch로 merge할 때 squash merge를 사용하면 안됩니다.

스크린샷 2022-10-24 오전 12 20 26

deva, b, c commit이 있고 main에 squash merge를 한다고 가정해봅시다.

스크린샷 2022-10-24 오전 12 22 25

squash merge 이후 상황은 위와 같습니다.
main branch는 서비스용 branch기 때문에, 추후 개발 기간동안 구현된 기능은 dev branch에 merge 될 것이고,
개발 완료된 기능들이 서비스로 release 될 때, dev에서 다시 main으로 merge가 될 것 입니다.

스크린샷 2022-10-24 오전 12 33 56

main으로의 release 이후 새로운 커밋 d, edev에 merge 되었습니다.
d, e를 release 하기 위해서는 우리는 다시 main으로 merge를 할 것 입니다.
d, e를 포함한 PR을 작성하는 순간 충돌이 발생하게 됩니다!

스크린샷 2022-10-24 오전 12 40 46

이는 main에 squash merge된 a, b, c commit이 a, b, c commit을 포함하고 있다는 사실을 알 수 없기 때문에 발생하는 현상입니다.
PR에 포함된 commit을 살펴보면 d, e 뿐만 아니라, 이전에 이미 main에 merge한 commit인 a, b, c도 commit 내역에 포함되어 있는 것을 볼 수 있습니다.
즉, main branch의 a, b, c commit 이전 조상을 maindev의 공통 조상으로 인식하게 됩니다.
이런 이유로 충돌이 발생합니다.

feature branch의 경우 해당 기능 구현 이후 사용되지 않을 branch이기 때문에 dev로의 squash merge 이후 발생할 문제가 없습니다.
하지만 dev branch의 경우 main에 merge된 이후라고 해도 이전 dev branch를 삭제하고 새로운 dev branch를 main에서 파생시키지 않는 이상 재활용되게 됩니다.
그렇기 때문에 두개의 base를 맞춰 주는 것이 중요합니다.

참고