git 에 대해 잘 모른 채로 어깨너머로 팀원들의 현란한 깃 명령어들만을 구경해 왔다.

이번 프로젝트 때 squash merge 라는 것을 사용하였는데, 이로 인해 예상하지 못했던 상황이 벌어졌었어서 이에 대해 정리 해 본다. 


상황 : 우리 프로젝트에서의 브랜치 전략과 merge 전략에 따른 문제점

 파이널 프로젝트에서 새로운 브랜치 전략을 도입하며, squash merge 를 적극 활용하기로 했다.

 

좋은시도였으나 우리모두 다음엔 이렇게 하지 말자..라는 결론이 지어진 방식이 되어버렸다 🥲  하지만 이는 조직마다 선택에 따라 달라질 문제고 이번에 우리가 선택한 방식에서 조금 변경하면 괜찮을수도? 있겠다는 결론을 내렸다. 이는 뒤에서 보자! 

 

우리의 브랜치 전략은

main, develop, feat 브랜치를 따로두고 

main -> 운영 서버 배포 

devleop -> 개발 서버 배포 

feat -> develop 으로 기능 추가하는 브랜치 

의 형식이었다. 

 

그런데 여기서 문제가 발생했다... 

 

우리는 모든 PR 에 대해  "squash and merge" 를 하고 있었다

🐳  우리가 squash merge 를 사용했던 이유
->  main, develop 에는 "기능 단위의 깔끔한 commit 이 남으면 좋겠다" .
develop 이나 main 으로 merge 할 때 마다 너무 많은 commit 이 섞이니, 커밋 내역만 보고는 기능단위로 커밋을 보기 힘들다는 의견이 존재했었다. 
우리는 squash merge 방식을 취할 때의 여파를 알지 못했고, 그것 참 좋은 의견 같습니다! 라고 말하며 모든 PR 에 대해 squash merge 를 하는 것으로  컨벤션을 정했다. 

충돌이 나요..!💥

main 에 develop 을 merge 한 이후, 기존의 develop 에서 다시 개발을 진행하여 두 번째로 main 에 merge 하려고 하자 모든 곳에서 conflict 가 나고 있었다. 

 

이로인해

squash merge 로 인해  main 브랜치에 merge 하여 운영 서버를 배포하고나면

 개발 브랜치를 아예 새로 만들어줘야 하는 상황이 발생했다

 

 


왜 이 런 일이 일어난 걸까???

git알못이던 나는 그저 "?_??" 이렇게 상황을 지켜보고 있었는데, 친절하신 우리 팀원분들이 알려주셨다. 

hash 값이 달라서 그래요.

 

hash 값이 달라서 그래요 -> 다른 commit 이에요... ?? 왜 다른 커밋이 된 거지?! 

우리의 브랜치를 살펴보자

먼저 현재는 종료된 우리 프로젝트의 브랜치를 확인해보자

저기 sprint-01,02,03 이 위에서 말했던 "main 에서 운영서버에 배포한 이후, 새로운 개발 브랜치를 파야 했던 것" 들에 해당한다. 

( 어떻게 보면  스프린트 단위의 개발 브랜치를 확인할 수 있게 되었다는 장점(?) 도 존재하는 것 같다..)


 

발생하던 상황

 

 

당시에는 상황에 대한 대략적 이해만 했었어서 

이 상황을 정리해보고자 직접 비슷한 상황을 만들어 테스트 해보았다

 

이 상황에서 추가적인 feat 이 commit 된 develop을 -> main 으로 다시 한 번 merge 하려고 할 경우, 다음과 같이 merge 할 수 없다는 오류가 뜬다. 

커밋 내역을 살펴보자 

그렇다면 아까 merge 된 이후의 main 브랜치에서는 어떤 커밋 내역이 있었는지 살펴보자.

같은 커밋 같지만......
hash 값이 다르다...!!!! 
그 이유는 squash merge 를 했기 때문이다

 

반면, 우리가 일반적으로 생각하는 merge 를 할 경우에는 

 

feat branch 에서의 commit 내역과,

main 에 merge 된 이후, 커밋 내역을 살펴보면, commit 에 대한 hash 값이 같음을 볼 수 있다. 

 


squash merge 가 뭔데 ???

develop 의 여러 commit history 를 합쳐서, 깔끔하게 하나의 commit 으로 main 에 merge 된다.

이와 같이 "게시글 목록 불러오기를 페이지네이션으로 변경"하여 구현하는 과정에서 발생한 모든 커밋들이, "1 개의 커밋" 으로서 커밋된다.

우리의 의도가 "commit history" 를 깔끔하게 만들기 위함 이었기에, 그 의도에는 적합한 merge 이기는 했다. 

 

하지만 문제가 존재했음

squash 는 extra commit 을 생성한다

위의 feat123 은 결국 feat1,feat2,feat3 과는 상관없는 "별개의 새로운 commit" 에 해당한다. 

 

따라서...

D 시점 이후 develop 에서 추가적인 기능을 개발하고 main에 또다시 squash and merge 하려고 할 경우 수많은 충돌이 나게 된다. 

main 입장에서는 dev 의 feat1 ~ feat 5 모두가 새로운 commit 에 해당되고, 만약 변경된 파일이 있으면 이들은 모두 conflict 로 인식되는 것이다. 

 

 

Merge 는 공통 조상으로부터 시작된 여러개의 커밋 히스토리들을 합치는 것인데,

사실상 squash 는, 공통 조상으로부터 시작된 이 브랜치에서 이루어진 커밋들을 "합치는 것" 보다는, 이들을 통해 "새로운 커밋을 만들어"버리는게 되는 것이다. 

 

그렇다면 여기서 "A" 와 "B" 의 공통조상은 누구일까? 

일반적인 merge 를 사용하고 있었다면 D 였을 텐데 ,이 경우에는 사실상 "D" 가 아닌, "C" 가 되는 것이다. 

따라서 충돌이 날 수 밖에 없다


우리는 어떻게 했고, develop 을 하나만 유지하려면 어떻게 했어야 했을까?

따라서 우리는 운영서버를 배포할 때 마다, main 으로부터 새로운 develop 브랜치를 만들었다. -> spirng01,02,03 .. 이라는 이름을 부여해주었고, 현재 개발중인 브랜치는 develop 으로 하였다.

 

만약 실제로 (추가적인 dev 브랜치를 생성 않고 ) 하나의 develop 브랜치만을 사용하면서, 커밋 히스토리를 깔끔하게 유지하고 싶었다면

  • commit history 를 깔끔하게 관리하기 위해, feat -> dev 는 squash and merge를 사용하고
  • 운영서버를 배포하는 main 에 대해서는 일반적인 merge 를 사용했어야 했던 것 같다. 

 

 

 

복사했습니다!