최근 현직 개발자 친구랑 프로젝트를 하면서 잘 못 알고 있던 사실을 제대로 알게 되었습니다. 사실 최근은 아니고 24년 말에 프로젝트를 진행하는 과정에서 알게 됐습니다..ㅎㅎ 커밋 히스토리를 관리함에 있어서 저는 각 커밋이 여러 분기에서 하나로 잘 뭉쳐지면 된다라고만 알고 있었고, 브랜치 전략을 세우거나 Merge를 하는 과정에서 그 부분에 대해 깊이 생각해 본 적이 없었습니다. 하지만 위에서 언급한 프로젝트 진행 과정에서 Linear하게 커밋 히스토리를 유지해야 하는 이유와 그 중요성에 대해 배우고 이해할 수 있었습니다. 그 내용에 대해 정리해보고자 합니다. 매번 규칙적으로 적는다 하고 적지를 못했네요ㅠㅠ 그래도 새로 알게되거나 배운 값진 내용들이 있으면 이렇게 종종 작성하겠습니다..!

 


Linear한 커밋 히스토리가 무엇인가?

프로젝트를 진행하는 과정에서 상용 서버와 연결된 main 브랜치에 작업한 내용들을 올리는 과정에서 포함하면 안되는 커밋을 제외하고 merge하거나 특정 커밋을 제거해야 하는 경우가 자주 있습니다. 이런 경우를 대비하여 커밋 히스토리는 선형으로 유지되어야 합니다. 그래야지 특정 시점의 커밋을 rebase -i나 cherry-pick을 통해 추가하거나 제거하기가 수월합니다. 이렇게만 작성하면 이해하기 어려우니 사례를 직접 보여드리겠습니다.

 

첫번째 이미지는 제가 제일 처음으로 진행했던 프로젝트의 커밋 히스토리 그래프이고,

두번째 이미지는 현직 개발자 친구와 함께 진행하는 프로젝트의 커밋 히스토리 그래프입니다.

 

첫번째의 경우 "[feat]리코프런..." 커밋을 제거하거나 수정하기 위해선 연결되어 있는 다른 커밋과의 충돌 및 관계를 고려해야 해서 정말 복잡한 작업이 될 것입니다. 이를 두 번째 경우에 대입하여  "fix:자료..." 커밋을 제거하거나 수정하고 싶다면, git rebase -i {변경하고자 하는 커밋 해시값의 이전값}을 터미널에 입력하여 vim에서 pick을 drop으로 바꾸소 :wq를 하면 그 커밋이 깔끔하게 제거됩니다. 만약 여러 커밋을 한 커밋으로 합치고 싶다면 vim에서 pick을 squash로 변경해 squash가 입력된 커밋끼리 합칠 수 있습니다. 해당 과정에서 충돌이 발생하면 충돌 해결 후 git rebase --continue를 충돌이 모두 해결될 때까지 반복하고, git push --force를 입력해 github 레포지토리에 적용할 수 있습니다.

 

그렇기 때문에 보통 최종 브랜치인 main과 개발 브랜치인 dev는 무조건 커밋 히스토리가 선형으로 유지되어야 합니다..!

 

 

Linear한 커밋 히스토리를 유지하는 방법 (main <-> dev 브랜치를 사용하는 경우)

제가 진행하고 있는 프로젝트에서는 운영 서버 브랜치 main, 개발 서버 브랜치 dev를 유지하고 feature 브랜치를 dev에 붙여서 dev를 main에 merge하는 방식으로 브랜치 전략을 취하고 있습니다. 여기서 디테일하게 말하자면 feature 브랜치를 dev(불가피하게 main에 붙이는 경우도 있습니다)에 merge할 때는 squash merge로 하고, dev를 main에 merge할 때는 rebase merge를 합니다. 이렇게 하는 이유를 자세하게 설명 드리겠습니다.

 

보통 특정 기능이나 페이지를 구현할 때, 이슈 생성 -> 이슈 기반 develop에서 뻗어나오는 feature 브랜치 생성 -> 작업을 하며 여러 커밋을 남긴 후 PR을 올려 dev에 merge하는 방식으로 진행할 것이라 예상됩니다. 이 과정에서 한 브랜치에 각 작업에 대한 많은 커밋이 쌓일 것이고, 해당 PR을 여러명이 동시에 작업하는 dev나 main 브랜치에 rebase나 그냥 merge를 한다면 그 시점이 꼬이고, 충돌이 매우 많이 발생할 것입니다. 그래서 feature 브랜치에 대한 여러 커밋이 쌓인 작업은 squash merge를 통해 하나로 묶어서 dev나 main 브랜치에 붙입니다. 그래야지 각 사람의 작업이 명확하게 분리될 것이고, 그 작업을 지우거나 수정해야 할 때 간단하게 처리할 수 있습니다. 이를 통해서 feature 브랜치를 dev나 main에 merge할 때, 선형적으로 커밋 히스토리를 유지할 수 있습니다.

 

이후, main과 dev를 mege하는 과정에서는 rebase merge를 합니다. rebase merge를 해야 각 커밋이 하나의 선으로 일치시킬 수 있기 때문입니다. 그림을 보면 빠르게 이해하실 수 있을거 같습니다.

출처) https://www.edureka.co/blog/git-rebase-vs-merge/amp/#What-is-Merging?

그림을 보면 알 수 있듯이 그냥 merge는 커밋을 다른 선에 유지시킨 상태에서 근간이 되는 선에 대한 분기 그래서 A를 수정할려면 3과의 충돌을 고려해야합니다. 하지만 리베이스의 경우 분기가 발생하지 않고 선형으로 유지되기 때문에 div와 main은 rebase merge를 이용합니다. 하지만 이렇게 rebase를 하는 경우 A와 B에 대한 커밋 내용을 보고 싶은데 찾기 어렵지 않을까라는 의문을 가질 수도 있다고 생각합니다. 이를 고려해 feature 브랜치를 main이나 dev에 붙일 때는 squash merge를 진행하는 것입니다. squash merge를 하면 해당 feature 브랜치에 올렸던 커밋 내용을 확인할 수 있기 때문에 기록을 확인할 수 있습니다. 그리고  이 과정에서 feature를 너무 크게 잡지 않는 것이 중요하다고 생각합니다.

 

그래서 특정 feature 브랜치에서 작업을 하고 있는 과정에서 develop 브랜치가 최신화 된다면, 바로 pull을 받는 것보다 dev 브랜치로 이동해 pull을 받아 최신화를 한 후 feature 브랜치로 돌아와 rebase를 하거나 `git fetch origin develop:develop`을 입력해 feature 브랜치에서 develop 브랜치를 최신화 시킨 후 git rebase develop을 하여 안전하게 feature 브랜치를 최신화합니다.

 

이러한 방법이 정답이 아닐 수 있지만, 현재 저의 입장에서는 프로젝트 관리 측면에서 해당 방법이 제일 유용하다고 생각합니다. 다른 더 좋은 방법이 있거나 틀린 내용이 있다면 알려주세요..! 오늘도 감사합니다 :)

+ Recent posts