브랜치 전략에서 squash and merge 방식 사용에 대한 고찰
들어가면서
우아한테크코스 레벨3에서 현재 팀 프로젝트(이하 하루스터디)를 진행하고 있다. 하루스터디 서비스 개발을 시작하기 전에 서비스 기획과 더불어서 팀 컨벤션을 정하는 회의를 많이 진행했다. 그 중 Git 브랜치 전략도 팀 컨벤션으로 정하였는데 이 기간에 예비군 훈련과 겹쳐서 직접적으로 팀원들과 브랜치 전략을 연습하고 정하는 시간에 함께하지 못했다. 이후 팀원들이 정리해둔 문서를 보며 혼자 브랜치 전략을 연습하던 와중 마주한 문제가 있었는데 이를 정리하고자 한다.
squash and merge 사용 시 직면한 문제상황
위 이미지에 나온 것 처럼, main 브랜치에서 프로덕션 코드를 관리하고 develop 브랜치에서 기능 구현 작업들이 완료되면 main 브랜치에 PR을 날리는 방식으로 운영한다고 가정했다. 브랜치 전략을 혼자 연습하는 과정에서 팀 내 컨벤션에 정해져 있는 것처럼 모든 merge 방식을 squash and merge만 사용했다.
develop 브랜치에서 1차적인 자기소개 문서 작업이 마무리되었다. 아래 이미지에 나와있는 본계정, 부계정 자기소개 커밋이 이 작업에 해당한다. 이 작업 내용을 main 브랜치에 squah and merge 방식으로 PR을 날렸다. 성공적으로 merge되어서 main 브랜치에 자기소개 V1.0 배포라는 하나의 커밋이 생겨난 것을 확인할 수 있다.
이후 develop 브랜치에서 새로운 기능구현 작업을 진행했다. '프로젝트 한 줄 소개' 작업을 추가적으로 완료하고 이를 V1.1로 main 브랜치에 PR을 날리려고 할 때 문제상황이 발생했다.
처음에는 develop 브랜치에서 main 브랜치로 merge 한 이후 추가 작업한 커밋들을 당연히 문제 없이 merge할 수 있을 것이라고 생각했다. 하지만 예상과는 다르게 develop → main 브랜치로의 merge 과정에서 충돌이 발생하는 문제가 발생했다.
문제원인
squash and merge 방식은 여러개의 커밋을 하나의 새로운 커밋으로 합쳐서 목표 브랜치에 merge하는 것이다. 이 때 결과적으로 하나의 새로운 커밋이 추가됨을 주목해야 한다. 이 새로운 커밋은 기존에 작업했던 커밋들과는 전혀 다른 커밋으로 여겨진다.
커밋 해시코드 값을 한 번 살펴보자. 새로 만들어진 자기소개 V1.0 커밋의 해시코드 값은 기존 두 커밋과 모두 다르다.
이 상황에서 develop 브랜치의 최신 커밋인 '프로젝트 소개 내용 추가' 커밋을 main으로 merge 시도하면 어떻게 될까? main 브랜치 입장에서는 변경 이력 추적이 안되고 있던 완전히 새로운 커밋이 merge 되려고 하는 것이다. 그래서 기존 상태와 다른 부분에 대해 모두 충돌이 났다고 판단하는 것이다.
해결방안 모색
일단은 develop → main 브랜치로 발생한 충돌을 해결해야 했다. 빠르게 찾아본 레퍼런스에서는 다음과 같은 방법들을 사용할 수 있다고 한다. (잘 정리해두셨던 선배 크루에게 감사합니다!)
revert
- main 브랜치의 최신 커밋들을 반대로 되돌리는 작업을 수행한다. 그래서 기존 develop 브랜치의 베이스까지 revert를 수행한 후 develop 브랜치 변경사항을 커밋한다.
- 장점
- 가장 안전한 방법으로 충돌을 해결할 수 있다.
- 단점
- main 브랜치에 revert 작업의 이력이 남는다.
- 기존 develop 브랜치에는 반영되지 않은 main 브랜치에만 커밋된 내용의 경우 revert 이후 개별적으로 커밋을 반영해줘야 한다.
- main 브랜치에 squash and merge 방식으로 버전 별 커밋을 남겨오던 방식을 고수할 수 없다.
cherry-pick
- main 브랜치 헤더에서 develop 브랜치에 새로 추가된 커밋을 cherry pick 명령을 사용해 바로 이어붙인다.
- 장점
- 원래 우리가 생각했던 방식으로 main 브랜치에 커밋을 추가해서 merge 할 수 있다.
- main 브랜치에 기존 커밋 이외 작업 이력이 따로 남지 않는다.
- 단점
- main 브랜치에 squash and merge 방식으로 버전 별 커밋을 남겨오던 방식을 고수할 수 없다.
- cherry pick으로 가져오는 모든 커밋마다 각각 충돌을 해결해줘야 한다.
force-push
- main 브랜치 상태를 무시하고 develop 브랜치 상태를 강제로 덮어 씌워버린다.
- 장점
- 가장 간단하고 편하게 충돌을 해결할 수 있다. (역사적으로 싹 밀어버리는 것이 약일 때가 많았다는 관점…)
- 별도의 작업 커밋 이력이 남지 않는다.
- 단점
- 기존 main 브랜치 작업 이력을 전부 날려야 한다.
- 만약 기존 main 브랜치에만 존재하는 커밋(squash and merge 커밋)을 베이스로 하는 브랜치를 따서 작업중이었던 경우 이를 수작업으로 원격저장소와 동기화를 해줘야 한다.
- force push 작업 간에 main 브랜치에 새로운 작업 이력이 발생하게 되면 누락되므로 이를 통제하는 작업이 추가적으로 오프라인 차원에서 이뤄져야 한다.
- 기존 main 브랜치 작업 이력을 전부 날려야 한다.
사실 현재 연습 레포에서는 main 브랜치와 develop 브랜치 작업량과 커밋 이력이 많지 않아서 어떤 방식을 채택해도 문제는 없다. 하지만 지금 상황이 이미 서비스가 제공되고 있는 상황이고, 이전 커밋 내용들과 이력들을 전부 파악하는게 불가능하다고 가정하고 해결책을 생각해보고자 했다.
일단은 cherry-pick 방식을 고려해볼 때 여러 장단점을 고려해봤다. 하지만 다른 것보다 main 브랜치에 기존 squash and merge 방식으로 병합된 커밋이 그대로 남게 된다는 것이 마음에 걸렸다. 우리가 develop → main 브랜치로의 병합 전략을 squash and merge 방식으로 수행했을 때 마주한 문제의 재발을 방지하기 위해서는 main 브랜치에서 squash and merge 방식으로 병합된 커밋을 없애주는 것이 필요하다 판단했다. 이러한 이유로 cherry-pick은 해결방안에서 제외했다.
남은 방식은 force-push와 revert 였다. force-push의 경우 main 브랜치의 기존 작업 이력들이 전부 삭제된다. 그렇기 때문에 main 브랜치의 작업 내용들이 완벽하게 develop 브랜치에 모두 존재해야 한다. 우리는 팀 내 컨벤션으로 항상 develop 브랜치를 거쳐서 main 브랜치로 merge되도록 했기 때문에 main 브랜치의 작업 이력들이 develop 브랜치에 모두 존재한다고 생각할 수 있다. 따라서 기존 main 브랜치에만 반영된 작업 이력이 누락될 가능성은 배재시킬 수 있다.
force-push의 단점을 커버할 수 있다는 판단하에 생각해보니 굳이 revert를 채택할 필요가 없다는 생각이 들었다. 두 방식 모두 squash and merge 방식으로 병합된 커밋을 main 브랜치에서 제거한다는 과정을 공통적으로 거치는데 굳이 revert 이력을 main 브랜치에 남기는 방법을 채택하고 싶지는 않았다. 이러한 고민을 거쳐 해당 문제를 해결하는데 force-push 방식을 선택하기로 결정했다.
충돌 해결 과정
develop 브랜치에서 원격 저장소의 main 브랜치로 force push를 수행해줬다. 이후 로컬에서 기존 main 브랜치를 삭제하고 fetch 작업을 해주니 다음과 같은 브랜치 그래프를 확인할 수 있었다.
main 브랜치에서 충돌을 일으키던 주 원인이었던 V1.0 squash and merge 커밋을 삭제하고 develop 브랜치 이력을 그대로 다시 가져올 수 있었다. 해당 과정에서 원래 main 브랜치로 merge하려고 했던 '프로젝트 소개' 커밋도 함께 merge 된 것을 확인할 수 있었다. 더불어 곁가지로 뻗어있던 다른 feature 브랜치 작업 내용도 그대로 유지됨까지 확인할 수 있었다.
더 알아볼 것
하루스터디 팀 브랜치 전략을 연습하며 발생했던 문제를 해결했다. 그런데 하나 마음에 걸린 것은 main 브랜치에서 버전 별 릴리즈 기점을 커밋으로 표현하기 힘들어 졌다는 것이다. 이는 깃허브에서 제공하는 Realease 기능을 활용하면 어느정도 해결할 수 있을 것으로 보이는데 시간 상 이는 다음에 적용해보고 정리하고자 한다.
위 본문에서 다루지는 못했지만 근본적인 문제 재발을 막기 위해서는 팀 내 컨벤션으로 정한 merge 전략이 좀 더 구체적인 방향으로 수정되어야 한다고 생각했다. 기존 feature 브랜치에서 기능 구현을 완료하고 develop 브랜치로 merge할 때에는 squash and merge 전략을 사용해도 문제가 없을 것이다. 핵심은 develop 브랜치에서 main(배포) 브랜치로 merge할 때인데 이 때 squash and merge가 아니라 일반 merge 방식에 --no-ff 옵션을 주는 방식으로 진행하면 괜찮을 것 같다는 생각이 들었다.
위 이미지에서 test1을 main(배포) 브랜치, test2를 develop 브랜치 라고 가정해보자. 해당 방식으로 진행하면 main(배포) 브랜치에 버전 별로 merge 커밋을 남길 수 있다는 장점도 가져갈 수 있을 것 같다. 더불어서 develop 브랜치를 삭제하지 않고 유지한 채로 새로운 기능 개발을 이어나갈 수 있을 것이다.
아직 이러한 문제에 대해 하루스터디 팀원들과 공유하지 못했는데 이후 팀 내 논의를 거쳐 main 브랜치로의 merge 전략을 다시 얘기해보아야 할 것이다.
(혹시 포스팅 중 틀린 내용이 있다면 피드백 댓글 주시면 감사하겠습니다!)
Reference
- https://velog.io/@betterfuture4/%EC%86%8D%EB%8B%A5%EC%86%8D%EB%8B%A5-Squash-Merge-%ED%9B%84-Rebase%EC%8B%9C-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%EA%B8%B0#%EB%B0%A9%EB%B2%95-3--force-push
- https://wikidocs.net/153871