[git] git merge, git pull 전략 - merge commit, rebase, ff, squash

기영·2024년 8월 13일

git

목록 보기
1/1

📌 포스팅 읽기에 앞서...
옳지 않은 정보가 있을 수도 있습니다.
정보 수정은 언제나 환영입니다!
옳지 않은 정보가 있다면 댓글로 남겨주시면 감사하겠습니다 👍


처음으로 협업을 하게되면서 브랜치를 통한 작업을 진행해나갔다.
급하게 작업을 하며 그냥 develop 브랜치에서 바로 작업을 한 뒤 push를 하려 했을 때 위와 같은 에러 문구를 수많이 겪었다.

정확하게 모르며 그냥 해결하기 위해서
$ git pull --rebase origin develop
를 입력하여 원격 develop 브랜치의 커밋을 로컬 develop에 다 땡겨와 어찌저찌 해결 시켜 넘어갔다.

해당 문제는 협업을 할 때 가장 기본이 되는 문제라 생각하여 해커톤이 끝난 후 가장 먼저 자세히 살펴보았다.


해당 포스팅을 위한 준비

진행 전 해당 포스팅에 사용될 용어와 환경들을 간략하게 정리해보자.

📌 용어 정리
원격 리포지토리 : 깃허브(웹 페이지) 상에서 생성한 리포지토리
로컬 리포지토리 : 원격 리포지토리와 연결된 내 PC 상의 리포지토리

현재 원격 리포지토리의 main 브랜치 내에는 README.md 파일만 존재한다.

현재 브랜치는 2개(main, develop)이 있으며, develop의 내용은 main과 동일.
develop에서 feature/* 브랜치들을 만들어 새로운 기능(파일)을 추가한다.

$ git clone {원격 리포지토리}를 통해 로컬 리포지토리를 생성하였다.

작업환경은 VS code이고, git graph 익스텐션을 사용하여 브랜치의 분기를 살펴본다.


📌 참고 !
git graph를 통해 보았을 때 origin/HEAD, origin/develop이 보인다.
이는 로컬 리포지토리와 연결된 원격 리포지토리의 브랜치들을 나타내는 것이다.

origin/HEAD = origin/main이다.
이는 디폴트 브랜치origin/HEAD가 되는 것으로 해당 내용은 어렵지 않으니 따로 검색을 해보기 바란다.
디폴트 브랜치 변경은 원격 리포지토리 깃허브 페이지에서 가능하다.


본문

앞서 본 에러는 develop 브랜치에 git push를 했을 때 일어나는 에러였다.

에러 문구에서 확인할 수 있듯이 해당 에러는 로컬에서 작업을 끝난 후 develop 브랜치로 push를 보낼 때 원격의 develop 브랜치와 커밋이 다르기 때문이다.

즉, 내가 develop에서 작업을 하던 도중 다른 누군가 develop에 자기 작업물을 merge한 것이다.

다음과 같은 상황이 있다고 가정하자.
다른 팀원이 작업을 마친 후, 로컬 → 원격 develop 브랜치로 push 시켰다.
그렇다면, 현재 원격 리포지토리의 develop 브랜치는 팀원의 커밋이 추가로 붙어있을 것이다.
이제, 내가 작업을 마치고 로컬 → 원격 develop 브랜치로 push를 시켰을 때 커밋 내역이 다르기 때문에 "내가 작업을 커밋이 어디 들어가야 하지?"라는 부분때문에 위와 같은 오류가 발생하는 것이다.

📌 "내가 feature/A, 팀원이 feature/B 브랜치를 따로 파서 push한 뒤 PR을 통해 develop에 합치는 건 왜 문제가 안 되는거지?

다음과 같은 생각이 들 수도 있지만, 이건 우리가 push를 한 건

  • 로컬 리포지토리의 feature/* → 원격 리포지토리의 feature/*

실제로 브랜치간 병합이 일어난 것은

  • 원격 리포지토리의 feature/* → 원격 리포지토리의 develop

이므로 로컬의 develop과 원격의 develop이 달라도 전혀 문제가 되지 않는 것이다.
단지, 작업이 끝난 후 로컬에서 develop 브랜치로 checkout 하여 $ git pull origin develop만 잘 해주면 된다.

위에 내용을 이어 이야기하자면, 에러 문구에 해결책으로 나온 것이 git pull을 통해 커밋 내용을 맞추라는 이야기이다.
즉, $ git pull origin develop을 통해
원격 develop 커밋 → 로컬 develop 커밋에 포함(동기화)시키라는 이야기이다.

git pull

git pull 전략에 대한 내용을 들어가기에 앞서 git pull에 대해 잠깐 알아보고자 한다.
왜냐하면, 'git pull rebase, ff'에 대해 검색해보았을 때 git merge ...를 통한 설명이 많았기 때문이다.

에러 설정은 git pull을 하라는 이야기인데, rebase, ff에 대해 검색하니 git merge로 이야기가 나와서 약긴 불편했었다.
그냥 내가 깃에 대해 잘 모르는 거였지만

처음에는 그저 git pull이 "원격 리포지토리 내용을 가져오는 거구나" 정도로 알고 있었다. 틀린 이야기는 아니다.

git pull은 git fetchgit merge가 합쳐진 명령어이다.
git fetch는 원격 리포지토리에 있던 내용을 내 로컬 리포지토리에 가져오고,
git merge는 체크아웃된 브랜치를 병합시킨다.

"git pull이 원격에 있던 내용을 내 로컬로 가져오는 거 아니였음?"

이 헷갈리는 개념은 예제 사진을 보면 이해하기가 쉽다.

앞서, vscode에서 $ git branch -al 명령어를 통해 로컬과 원격의 리포지토리를 확인하였다.
❗️ 이때, 빨간 색으로 보이는 remote/origin/*는 원격 리포지토리를 나타내긴 하지만, 원격에 있는 브랜치와 동일하지는 않다.

git fetch 명령어를 입력하면 원격에 있는 브랜치의 내용을 가져와 remote/origin/{브랜치명}에 원격에 있는 브랜치 내용을 저장한다.

로컬에 origin/{브랜치명}이 없으면 checkout을 통해 origin/{브랜치명}을 만든 뒤에 거기에 원격 브랜치 내용을 저장한다.

이후 git merge를 통해 로컬에서 origin/{브랜치명}{브랜치명}으로 병합하는 것이다.

git pull 전략

자 이제 git pull 전략에 대해 알아보자.

  • merge commit
  • squash
  • rebase
  • ff

우선 develop 브랜치를 로컬에서 생성해준다.

feature/A 브랜치를 만들어준다.
현재 초기 시점(커밋)에 feature/A , develop, main, origin/HEAD가 위치한 것을 확인할 수 있다.

feature/A에서 A.txt를 만든다.
현재 커밋을 보내지 않았기 떄문에 uncommitted changes라 뜬다.

A.txt를 커밋(make A)까지 해주니 feature/A가 초기 위치의 브랜치들의 모임에서 빠져나온 것을 확인할 수 있다.

A 파일에 수정을 하여 새로운 커밋을 생성한다.
똑같이 uncommitted changes라 뜬다. 이제 수정했다는 커밋을 보내보자.

modify A 커밋 시점으로 feature/A가 위치한 것을 확인할 수 있다.


📌 이제 feature/A를 develop에 합쳐보자

develop 브랜치로 바꾸자 현재 위치가 맨 아래로 돌아간 것을 확인할 수 있다.

아무 옵션도 주지않고 feature/A를 develop에 병합을 하자 develop 태그가 featureA의 분기로 간 것을 알 수 있다.
이는 ff 방식으로, 병합하려는 브랜치가 ff 관계이면 알아서 ff 방식 병합한다.


실습을 통해 알아보는 git pull 전략 예제

feature/B 생성 후 파일을 하나 만들고 여러 커밋이 발생했다.

이 상태에서 develop으로 돌아오면 B.txt 파일이 삭제된 것을 알 수 있다.
B.txt는 feature/B에서 생성한 파일이니 없어지는 게 당연하다.

📌 이 때, develop에 새로운 파일을 하나 만들어 보자.

develop에서 새로운 파일 Develop.txt를 만들면 git graph 분기가 생긴 것을 확인할 수 있다.
feature/A를 테스트했을 때와 달리 developfeature/B의 커밋이 다르기 때문이다.

develop의 변경사항을 커밋을 하면 develop 태그의 위치가 바뀐 것을 알 수 있다.


📌 이제 feature/Bdevelop로 머지를 시켜보며 각 merge 전략을 테스트해보겠습니다

1. merge commit

📗 merge commit
merge commit은 병합 시 브랜치 내 모든 커밋을 베이스 브랜치로 합친 뒤 병합 시점의 커밋 메시지도 추가로 남기는 전략이다.

$ git merge feature/B를 하자 다음과 같은 문구가 떴다.

병합하려는 developfeature/B의 커밋의 차이가 발생하였기 때문에 $ git merge feature/B 명령어 실행 시 merge commit으로 병합이 진행된다.

따라서, 터미널에 feature/Bdevelop에 병합 되는 시점의 커밋을 남겨야한다는 문구가 나온다.
i를 눌러 vim 입력 모드로 들어가준 뒤 커밋 메시지를 작성해줍니다.


커밋 메시지를 작성 후 :wq를 입력하여 커밋 메시지 작성을 완료하면 다음과 같이 병합된 시점에 커밋이 하나 생기며 병합된 것을 확인할 수 있다.

다른 전략을 테스트해보기 위해 $ git reset HEAD^로 최근 커밋을 지우겠다.

그럼 다음과 같이 병합 하기전 상태로 돌아온 것을 확인할 수 있다.
참고로 B.txt 파일이 untracked 상태로 develop에 남아있다면 discrad 해버리면 된다.

2. squash and merge

📗 squash and merge
squash and merge는 병합 시 브랜치 내 모든 커밋을 하나의 커밋 메시지로 통합시켜 베이스 브랜치에 해당 커밋을 반영하여 병합하는 전략이다.

$ git merge --squash feature/B를 통해 squash merge를 하니 다음과 같은 문구와 커밋 메시지가 나온다.

과연 뭘까?

squash and merge는 브랜치의 모든 커밋들을 하나의 커밋으로 통일시켜 베이스 브랜치에 합치기 때문에 그 커밋의 메시지를 정해줘야하는 것이다.

커밋 메시지는 'squash and merge - feature/B to develop' 으로 하겠다.


다음과 같이 feature/B 브랜치의 커밋은 develop의 그래프에 합쳐지지 않고, develop에 커밋 하나만 남겨져있는 것을 확인할 수 있다.

$ git log를 통해 살펴보면 develop에선 A에 대한 커밋은 확인 가능하지만, B에 대한 커밋은 나타나지 않으며, 병합 시 남겼던 커밋 'squash and merge - feature/B to develop'만 확인이 가능하다.

다시 리셋을 통해 병합하기 이전 시점으로 돌아간다.
앞서 설명하였듯이 B.txt 파일이 남아있다면 discrad해버리면 된다.


3. rebase

📗 rebase
rebase은 병합 시 브랜치 내 모든 커밋을 베이스 브랜치의 끝에 추가하는 병합 전략이다.

삭제한 뒤 원래 상태로 돌아왔다.
이제 rebase 전략을 확인해보자.

$ git rebase feature/B 입력 후 git graph를 보면 분기가 사라진 것을 확인할 수 있다.

당연하다.
rebase는 해당 브랜치의 커밋을 베이스 브랜치로 모두 옮긴다는 것이기 때문이다.
따라서, feature/B는 modifyB 커밋에 머물러 있고, develop은 modifyB를 자신의 커밋에 포함 시키고 있습니다.

📌 참고
rebase를 하면 feature/B의 커밋이 develop 커밋 맨 뒤에 붙어야한다.
그러나, 이 예제에선 'make Develop'이 맨 뒤에 붙어있는 것을 나중에 확인했습니다..

그 이유는 feature/B가 만들어지고 난 다음 develop에서 Develop.txt가 생성되었기 때문에 당연히 feature/B의 커밋 내에선 Develop.txt 관련된 커밋이 없지만, develop은 'make develop' 커밋에 있으므로 맨 마지막에는 make develop 커밋이 자리잡게 되었다.

해당 사항을 확인하여, develop에 추가적인 커밋없이 feature/D를 만들고(ff 관계) rebase를 테스트해보았다.
정상적으로 feature/D의 커밋이 맨 마지막에 이어 붙고, developfeature/D가 맨 끝 커밋을 가르키고 있음을 알 수 있다.


4. fast forward

📗 fast forward
ff은 병합하려는 두 브랜치가 ff 관계일 때, 베이스 브랜치의 위치를 병합하려는 브랜치의 최종 커밋 위치로 옮기는 것이다.

develop에서 feature/C를 만들고 'make C', 'modify C' 커밋을 남겼다.
분명 feature/C를 만들었는데 한 줄의 그래프로 나와서 당황할 수 있겠지만 구부러진 그래프가 원래 한 줄 모양이기 때문에 이렇게 나오는 것이다.

다르게 생각하면 한 줄이어야 fast forward 관계라고 할 수 있다.

fast forward는 베이스 브랜치 위치를 타겟 브랜치로 맨 마지막 커밋 위치로 옮기는 것이다.
당연히 한 줄에 있기 때문에 커밋 순서 등이 헷갈릴 일이 없고, git lens에선 태그의 위치만 바꿔주게 되는 것이다..!

태그 위치만 옮기는 것이기에 커밋 메시지를 따로 남길 필요가 없다 !
이것이 ff 관계 여부에 따른 merge commit과 fast forward의 차이이다.

병합을 할 두 브랜치가 ff 상태이면 $ git merge시 자동으로 ff로 머지가 된다.

그러나 병합 시점의 병합이 되었다는 커밋을 남기고 싶다면?
$ git merge --no-ff feature/*


git pull 전략을 통한 커밋 충돌 문제 해결

자 그러면 이제 초반에 일어났던 에러를 해결해보자!

$ git push origin develop 시 에러가 난 상태는 위와 같은 상태이다.

에러를 해결하기 위해 $ git pull --rebase origin develop을 한다면?

$ git pull --rebase origin develop을 통해 원격 브랜치와 로컬 브랜치를 동기화 시켜놓았기 때문에 이제 push에 문제가 없다!

profile
computer engineering student

0개의 댓글