M회사는 신규 웹 프로젝트 개발을 시작하려고 합니다. S팀장을 포함해 총 7명의 개발자가 팀에 속하게 되었습니다. 새롭게 시작하는만큼 준비해야 할 것도 많았습니다. 그 중 하나는 협업 규칙을 잘 정해두는 것이었는데요, 이를 통해 전체 소스코드의 일관성과 통일성을 유지하고 새로운 팀원이 들어왔을 때 보다 쉽게 팀에 녹아들 수 있기를 기대할 수 있었습니다.
여러 규칙들을 정하다보니 Git 협업 방식을 어떻게 가져갈지에 대한 이야기가 나왔습니다. 특히, 프로젝트에 참여하는 개발자가 7명이나 되기 때문에 커밋 히스토리 관리 방식을 사전에 잘 정의할 필요가 있었습니다.
팀장인 S씨는 누구보다 커밋 히스토리를 잘 관리하고 싶어했는데, 특히 커밋 히스토리를 복잡하지 않고 한 눈에 볼 수 있기를 원했습니다. 함께 일하는 개발자가 많아질수록 히스토리는 복잡해지고 알아보기 힘든 구조가 되기 때문입니다. 나중에 팀이 작업한 이력을 봐야 할 상황이 생길 때 이를 매끄럽게 보기 위해서는 사전에 규칙을 잘 정해야한다고 합니다.
S씨는 신입사원 L씨에게 어떤 방식으로 커밋 히스토리를 관리하면 팀 입장에서 생산성을 보장할 수 있을지 생각을 해보라고 과제를 던져줬습니다.
L씨는 이전에 실제로 커밋 히스토리가 복잡해서 문제를 느꼈던 프로젝트에 참여했던 경험이 있었습니다. 해당 프로젝트에서의 커밋 기록을 다시 확인해보며, 커밋 히스토리를 복잡하게 만드는 원인을 분석할 필요가 있었습니다.
눈으로 보기에도 너무 많은 브랜치들이 존재하고 있고, 브랜치간의 의존도가 너무 복잡합니다. 또한, 실제 개발 내용과 무관한 커밋들이 너무 많이 존재하고 있었습니다.반대로 생각했을 때, 브랜치를 복잡하지 않게 관리
하고, 실제 개발 내용과 관련있는 커밋만 관리
할 수 있다면 develop 브랜치를 보다 깔끔하게 관리할 수 있을 것 같습니다.
자료를 조사하던 L씨는 git의 merge방식을 이용해 커밋 히스토리를 관리할 수 있다는 글을 보았고, 해당 부분을 더 공부해보기로 했습니다. 그 결과 merge의 3가지 방식을 알게되었고, 직접 실습해보며 각 방식이 만들어내는 그래프의 모양과 장단점을 학인할 수 있었습니다.
이전 팀회의에서 git flow 버전 관리 방식을 이용하기로 이미 결정 된 상태입니다.
우선, 팀원 모두가 develop
브랜치를 공유합니다. 해당 브랜치는 기능 개발이 완료된 커밋들만 남게 됩니다.
이후, 각자 기능 개발을 위해 develop의 HEAD에서 feature
브랜치를 딴 뒤에 개발합니다.
개발이 끝난 이후에는 다시 develop
브랜치로 자신의 작업 내용을 merge
한다는 공통 규칙이 정해진 상태입니다.
위와 같은 방식을 fast-forward merge
라고 합니다. 현재 브랜치(develop)의 HEAD를 합칠 브랜치(feature/task)의 HEAD로 이동시키는 것입니다. 하지만, 해당 방식은 feature 브랜치 개발을 하던 중 develop 브랜치에 새로운 커밋이 추가되면 적용되지 못합니다. 어떤 브랜치의 HEAD가 올바른 코드인지 알 수 없기 때문입니다. (작업 영역이 동일하다면 conflict가 나기도 합니다.)
예를 들어, 다른 팀원 누군가 feature 브랜치에서의 개발을 완료하고 develop 브랜치에 merge를 수행한 상황일 수 있습니다.
상황을 위와 같이 맞추어두고, L씨는 여러가지 merge 방식을 테스트 해보았습니다.
merge commit을 생성하는 것은 가장 일반적인 방법입니다.
합치고 싶은 브랜치의 커밋을 참조해 새로운 커밋을 만듭니다. 위의 경우에는, [C2+C3+C4]를 참조하는 C7 커밋이 새로 생겼습니다. 또한, develop 브랜치와 feature 브랜치가 온전하게 연결되는 것을 확인할 수 있습니다.
해당 방식의 특징으로는, 커밋 히스토리에 정말 많은 기록들이 남게된다는 겁니다. develop의 어느 커밋으로부터 새로운 작업을 시작했는지 알 수 있고, 각 feature 브랜치의 커밋이 그대로 남아 작업 내역을 보존할 수 있습니다. 또한, merge가 되는 시점에도 기록이 남습니다.
기록이 남는 것이 무조건 좋지는 않습니다. 너무 자세한 기록이 남기 때문에 히스토리 그래프가 복잡해질 가능성이 높기 때문입니다. 브랜치가 많아지거나 merge가 자주 일어난다면 그래프의 가독성이 떨어질 수 밖에 없습니다. 예를 들어, 간단한 버그수정을 위해 커밋 1~2개로 이루어진 작은 길이의 feature branch가 있을수도 있는데, 이런 branch가 develop의 히스토리에 남는 것과 거의 무조건 merge commit이 생긴다는 점은 그래프를 더럽히기에 충분한 예시라고 생각합니다.
따라서, merge commit을 만드는 방식을 사용하는 경우에는 S팀장의 요구사항을 전혀 반영할 수 없다고 판단했습니다. develop 브랜치에서 파생된 모든 branch가 히스토리에 남게되고, 쓸데없는 merge 커밋이 생겨 커밋 내용을 확인하기 어렵게 만들기 때문입니다.
추가로, merge commit은 가장 일반적인 방식이기 때문에 혼자 개발을 하는 경우에 사용하기 적합한 방식이라고 생각했습니다. 왜냐하면, 혼자 개발을 한다면 여러 기능을 나누어서 개발하는 경우가 적어서 develop 브랜치의 HEAD가 feature 브랜치의 base와 동일함이 어느정도 보장되기 때문입니다.
또 다른 방식으로 squash and merge
가 있습니다. feature 브랜치를 따서 작업했던 모든 커밋 들을 합쳐 하나의 커밋으로 만들고 develop 브랜치로 병합하는 방식입니다.
위의 경우에 [C2+C3+C4] 커밋이 C7로 합쳐졌으며, 특이한 점으로는 C7 커밋이 feature 브랜치를 참조하지 않는다는 점이 눈에 띕니다. 참조하지 않는다는 것은 develop 브랜치를 바라봤을 때 feature 브랜치가 보이지 않는다는 뜻입니다. 따라서 보다 깔끔하게 develop 브랜치를 관리할 수 있게 됩니다.
해당 방식을 사용하는 경우, 커밋을 하나의 브랜치에서 관리할 수 있어 그래프가 깔끔해진다는 장점이 있습니다. 하지만, feature 브랜치에서 작업했던 모든 커밋들을 단 하나로 합치기 때문에, 세부적으로 어떤 작업을 나누어서 했는지 알기 힘들며, 커밋 단위가 매우 커질 가능성이 존재합니다.
해당 방식은, 브랜치를 정말 작은 단위의 feature로 잘
나누었을 때 사용하면 좋을 것 같습니다. 하지만, 잘 나눈다는 것은 혹시나 잘못 나누었을 때 커밋 단위가 매우 커질 수 있기 때문에 다소 아쉬움이 남는 방식이라고 생각했습니다.
마지막으로 알아본 방식은 rebase and merge
방식입니다. rebase라는 이름은 base를 새롭게 한다는 의미를 가지고 있음을 암시적으로 나타냅니다. 특정 브랜치의 base를 내가 원하는 브랜치의 HEAD로 옮길 때 사용합니다.
다시 한 번 처음 상황이 어땠는지 확인을 해보겠습니다.
위의 그림에서 feature 브랜치는 C1을 base로 가지고 있는데, feature의 base를 develop 브랜치의 HEAD로 옮기면 자연스럽게 두 브랜치를 합칠 수 있다는 아이디어입니다. 따라서, feature의 모든 커밋들이 develop으로 옮겨 붙기를 원합니다.
하지만 엄밀히 말하면, 커밋을 그대로 옮기는 것이 아니라 기존과 동일한 내용의 커밋을 생성해 붙이는 것입니다. commit id
를 확인하면 이를 알 수 있는데, 기존 브랜치 히스토리들의 commit id와 새로 붙은 히스토리들의 commit id를 비교하면 다르다는 것을 확인할 수 있습니다. (상단 그림에서 새로 붙은 커밋들을 [#C2, #C3]등으로 표현한 이유입니다.)
이후, develop 브랜치에서 feature 브랜치를 대상으로 fast-foward merge를 하면 develop의 HEAD가 feature의 HEAD로 이동하게 됩니다. (optional: feature 브랜치를 지워주면 더 깔끔해집니다.)
그림을 보면 알겠지만, rebase and merge를 사용하는 경우 모든 커밋을 하나의 브랜치에서 작업한 것 처럼 보이게 할 수 있습니다. 또한, squash and merge에서는 남기지 못했던 feature 브랜치의 모든 commit 기록들을 그대로 가져다 사용할 수 있습니다.
하지만, 어느 시점에 어떤 기능을 구현하기 시작했는지 등의 세세한 부분을 체크하기가 어렵습니다. 이러한 부분들은 PR기록을 잘 남기는 것으로 해결할 수 있습니다.
rebase에서 가장 큰 문제가 있다면, 히스토리 자체를 옮기는 것이기 때문에 충돌이 나는 경우 커밋마다 충돌을 해결을 해줘야 할 수 있다는 것입니다. 다른 방식들은 merge commit이 존재하기 때문에 한 번만 처리해주면 됩니다. 이를 해결하기 위해서는 feature에서 너무 오랜시간 작업을 하면 안됩니다. 또한 feature의 단위를 너무 크게 잡아서도 안됩니다.
L씨는 커밋 히스토리를 잘 관리하자는 관점에서 rebase and merge 방식을 제안했고 S팀장은 이를 채택했습니다. 대신, feature 단위를 작게 가져가면서 충돌이 발생할 확률을 최대한 낮추기로 했습니다. 팀의 히스토리 그래프는 깔끔해졌고, 생산성이 높아져 프로젝트를 성공적으로 마무리 할 수 있었습니다. 그리고 L씨는 공로를 인정받아 연봉이 대폭 올랐다는 해피엔딩..!