[Git] 8장 병합과 충돌

참조

병합(Merge)

  • 브랜치를 생성하는 목적은 원본 코드에 영향을 주지 않고 분리하여 개발하기 위해서입니다.
  • 독립된 브랜치에서 개발 작업이 끝나면 다시 원본 브랜치에 작업한 결과를 반영해야 합니다.
  • 분리된 브랜치를 한 브랜치로 합치는 작업을 병합(Merge) 라고 합니다.
  • 두 코드를 하나씩 직접 비교해 가며 수동으로 병합하거나 깃 같은 도구를 사용하여 자동으로 병합할 수 있습니다.

하나씩 직접 비교하는 수동 병합

  • 소스 코드가 2개 있다고 합시다.
  • 하나는 원본 소스 코드고, 또 하나는 수정된 소스 코드입니다.
  • 수정된 코드는 버그를 고치고, 새로운 기능을 적용하여 테스트한 코드입니다.
  • 개발을 끝내고 테스트를 완료했다면 수정 내역을 원본 소스 코드에 모두 반영해 주어야 합니다. 또는 수정된 코드 자세로 원본 코드를 대체할 수도 있습니다.
  • 수동으로 병합하려면 양쪽 파일을 일일이 비교하며 바뀐 점을 찾아서 적용해야 합니다.
  • 하지만 오류없이 코드를 병합하는 것은 간단하지 않습니다.
  • 만약에 소스코드가 2개보다 많다면 어떻게 될까요?
  • 예를 들어 원본 소스 A 를 복제하여 B를 수정하고, B를 복제하여 C도 수정하고 있습니다.
  • 요즘은 협업 때문에 다수 개발자가 각각 새로운 기능들을 만듭니다.
  • 이처럼 여러 개발자와 코드를 공유하면서 변경된 소스 코드를 병합하는 것은 매우 복잡합니다.
  • 수정이 완료되면 B는 원본소스 A에 수정 내역을 반영하는 작업을 해야 하고, C 또한 B를 반영하여 수정된 원본 소스 A에 다시 수정 작업을 반영해야 합니다.
  • 병합하려면 코드의 변경된 시점을 기억해야 합니다. 변경된 시점을 기준으로 순차적으로 병합 처리를 하는 것이 좀 더 쉽습니다.
  • 변경된 시간 순서를 따르지 않고 병합하면 코드가 영켜 잘못 동작할 확률이 매우 높습니다.

Git으로 자동 병합

  • 깃을 사용하면 복잡한 파일을 좀 더 간편하게 병합할 수 있습니다.
  • 깃의 자동 병합은 원본을 기준으로 두 파일의 변경 이력을 비교합니다.
  • 변경된 파일 내용이 발견되면 자동으로 수정된 코드 내용을 병합합니다.
  • 깃의 병합은 브랜치를 기준으로 합니다. 브랜치는 같은 저장소 내에서 서로 독립적인 작업을 분리한 영역입니다.
  • 분리된 각각의 브랜치에서 수정된 사항을 하나의 브랜치로 병합합니다.
  • 병합하고자 하는 브랜치는 같은 로컬 저장소에 있어야 합니다.
  • 과거에는 모든 병합 작업을 수종으로 해서 어려웠지만, 이제는 이러한 수동 병합 작업을 깃이 대신 처리합니다.
  • 하지만 깃이 모든 코드의 병합을 완벽하게 처리할 수는 없습니다.
  • 아무리 좋은 도구라고 해도 자동으로 반영하지는 못하는 것들이 있습니다.
  • 이를 충돌(Conflict) 라고 합니다.

병합(Merge) 방식

  • 깃의 병합은 브랜치를 기반으로 실행합니다.
  • 각 브랜치를 비교하여 자동 병합하는 형태입니다. 따라서 병합하려면 브랜치를 만들어 브랜치 안에서 수정 작업을 해야 합니다.
  • 사실 파일을 병합하는 것은 그렇게 간단한 일이 아닙니다.
  • 어떤 내용을 수정했고, 새로 추가했는 지 판별하기가 어렵기 때문입니다.
  • 병합할 때는 상대적인 기준을 판별하는 알고리즘들이 존재합니다.
  • 이 알고리즘들은 기준점과 수정 사항을 병합하는 처리 로직에 따라 다릅니다.
    • Fast-Forward 병합
    • 3-way 병합

Fast-Forward 병합

  • 깃의 가장 간단한 브랜치 병합은 Fast-Forward 방식입니다.
  • 일반적으로 Fast-Forward 병합 방식은 혼자 개발할 때 사용합니다.
  • 혼자 개발할 때는 브랜치가 생성된 커밋에 따라 순차적으로 분기됩니다.
  • 또 코드 수정도 순차적으로 할 때가 많습니다.
  • 즉, 브랜치가 분기되지만 전체 커밋 그림으로 보면 모든 변경사항은 순차적으로 진행됩니다.
  • 이어서 순차적 커밋에 맞추어 병합을 처리하는 방법이 Fast-Forward 병합입니다.

Fast-Forward 병합 실습

1. 실습을 위해 feature 브랜치 추가

  • Fast-Forward 병합 실습을 위해, feature 브랜치 하나를 생성합니다.

2. feature 브랜치에서 작업 진행

  • master 브랜치에서 feature 브랜치로 checkout 진행합니다.
  • feature 브랜치에서 index.html 내용 수정 작업을 진행합니다.
  • 수정이 완료 되면 커밋(commit) 까지 완료합니다.

3. master 브랜치로 checkout 진행 후 병합

  • 브랜치를 병합하려면 기준과 대상이 있어야 합니다.
  • 기준은 체크아웃된 현재 브랜치입니다. 따라서 병합하려면 먼저 기준이 되는 브랜치로 이동해야 합니다.
  • 우리는 master 브랜치에서 feature 브랜치로 분기하여 작업했습니다.
  • 작업한 feature 브랜치를 다시 master 브랜치로 병합할 것입니다.
  • 병합을 하려면 먼저 master 브랜치로 checkout 후 병합을 진행하면 됩니다.
$ git merge 브랜치이름

3-way 병합(Merge)

  • 3-way 병합은 좀 더 복잡한 병합을 처리할 수 있는 방법입니다.
  • 여러 개발자와 협업으로 작업하는 경우 대부분 3-way 병합을 사용합니다.

3-way 병합 실습

1. 3-way 병합 실습을 위한 브랜치 생성

  • hotfix 브랜치 하나를 새롭게 생성합니다.

2. 마스터 변경

  • hotfix 브랜치 하나를 생성하였고, 현재는 총 2개의 브랜치(master, hotfix) 가 있습니다.
  • 다시 master 브랜치로 checkout 하고 master 브랜치에서 index.html 내용 수정 작업을 진행합니다.
  • 작업이 완료되면 master 브랜치 checkout 상태에서 commit 을 진행합니다.

3. hotfix 브랜치 작업 및 커밋

  • master 브랜치 checkout 하고 내용 수정 후 커밋까지 완료하였습니다.
  • 다시 hotfix 브랜치로 checkout 진행하여 index.html 내용 수정 후, 커밋합니다.
  • master 와 hotfix 브랜치 내용이 둘다 수정되어 graph 모양이 분기 된 것을 보실 수 있습니다.

4. 공통 분모 찾기

  • 앞의 실습에서 브랜치별로 각각 커밋하여 두 브랜치가 갈라지는 모습을 확인했습니다.
  • 이처럼 브랜치 모양이 갈라지는 형태로 나뉠 때는 Fast-Forward 방식의 알고리즘을 적용하여 병합할 수 없습니다.
  • 이떄는 다른 병합 알고리즘인 3-way 방식을 이용해야 합니다.
  • 두 브랜치를 병합하려면 먼저 분할 기준인 공통 커밋 을 찾아야 합니다. 이를 공통 조상 커밋 이라고 합니다.
  • 공통 조상 커밋을 포함하는 브랜치와 새로운 두 브랜치, 이렇게 3개를 하나로 병합해야 합니다.
  • 깃은 3-way 병합을 할 때 공통 조장 커밋을 자동으로 찾아줍니다.

5. 병합 진행하기

  • 병합은 각 브랜치에서 독립적으로 작업된 소스를 파일 하나로 결합합니다.
  • 하지만 브랜치 별로 각각 작업한 내용을 병합하는 과정은 힘들고 어렵습니다.
  • 하지만 깃을 사용하면 복잡한 병합도 좀 더 손쉽게 처리할 수 있습니다.
  • 3-way merge 는 두 브랜치에서 공통 조상 커밋을 자동으로 찾아 주며, 공통 조상 커밋을 기준으로 브랜치를 병합합니다.

병합 메시지

  • 3-way 병합을 간단히 실습해 보았습니다.
  • 앞에서 3-way 병합을 할 때 새로운 병합 커밋이 생성되었습니다. 3-way 병합은 Fast-Forward 병합과 달리 병합 메시지가 필요합니다.
  • 깃은 두 브랜치를 병합한 후에 새로운 커밋을 하면 동시에 메시지를 자동 생성합니다.
  • 자동으로 작성되는 메시지 외에 직접 커밋 메시지를 작성할 수도 있습니다.
  • merge 명령어를 실행할 때 -e 또는 -edit 옵션을 사용하면 됩니다.
$ git merge 브랜치 이름 - edit

브랜치 삭제

  • 브랜치를 병합한 후에는 병합한 브랜치를 어떻게 관리할 지 결정해야 합니다.
  • 일반적으로 병합한 이후에는 병합된 브랜치를 삭제합니다. 하지만 지속적인 통합과 개발을 해야 하는 브랜치라면 병합 후에도 계속 남겨 둡니다.

깃 플로(git flow)

  • 깃 플로(git flow)는 브랜치 관리 기법입니다.
  • 수많은 브랜치 작업을 규격화해서 브랜치를 쉽게 다룰 수 있도록 하는 전력이라고 생각하면 됩니다.
  • 깃 플로에는 기본적으로 master, feature, develop, release, hotfix 브랜치가 있습니다.
  • 이 중에서 develop 브랜치는 master 브랜치에 병합한 후에도 삭제하지 않고 계속 유지합니다.
  • 이처럼 오랫동안 유지하는 브랜치를 long-running 브랜치라고 합니다.

병합 후 삭제

  • 병합된 브랜치의 커밋은 모두 원본 브랜치에 적용됩니다.
  • 따라서 중복되는 커밋을 가지는 별도의 브랜치를 유지할 필요는 없습니다.
  • 불필요한 브랜치는 삭제합니다.
  • 브랜치를 삭제할 때는 -d 옵션 을 사용합니다.
  • 참고로 -d 옵션 은 병합을 완료한 브랜치만 삭제할 수 있습니다.
  • 따라서 실습에서 성공적으로 병합한 feature 브랜치는 -d 옵션으로 삭제할 수 있습니다.
  • 병합을 완료하지 않은 브랜치를 삭제하고 싶다면 대문자 -D 옵션 을 사용해야 합니다.
$ git branch -d 브랜치이름

충돌(Conflict)

충돌(Conflict) 이 생기는 상황

  • 여러 사람과 개발 작업을 하다 보면 예상외로 충돌이 자주 발생합니다.
  • 대부분의 충돌 원인은 같은 위치의 코드를 동시에 수정했기 때문입니다.
  • 파일을 수정할 때 여러 개발자가 서로 다른 위치를 수정했다면 깃에서 서로 다른 위치의 소스를 자동으로 병합하기 때문에 문제가 없습니다.
  • 하지만 파일에서 동일한 위치에 두 명 이상이 서로 다르게 수정했다면 충돌이 발생합니다.
  • 즉, 같은 위치를 동시에 수정하면 두 수정 중 어떤것이 맞는지 깃에서 자동으로 알 수 없기 때문에 충돌(conflict) 이 발생합니다.
  • 이때 깃은 사용자에게 충돌 오류라고 알려주고, 개발자에게 직접 수정하여 충돌을 해결하라고 요쳥합니다.
  • 이러한 병합 충돌은 상당히 자주 발생합니다.

충돌(Conflict) 실습

1. feature 브랜치 생성

  • feature 브랜치를 하나 새롭게 생성합니다.

2. feature 브랜치 checkout 후, index.html 파일 수정 및 커밋

  • 생성된 feature 브랜치를 checkout 합니다.
  • checkout 완료된 후, index.html 파일을 수정하고 커밋합니다.

3. master 브랜치 checkout 후, index.html 파일 수정 및 커밋

  • feature 브랜치에서 작업 완료 후, 커밋이 완료 되었다면 master 브랜치로 checkout 합니다.
  • checkout 완료된 후, index.html 파일을 수정하고 커밋합니다.

4. 3-way 병합으로 Merge 진행

  • 현재 feature, master 2개의 브랜치 모두 동일한 파일인 index.html 내용 수정 후 커밋을 하였습니다.
  • 공통 부모 커밋은 같지만 feature, master 모두 변경이 되어서 3-way 병합 준비가 된 상태입니다.

5. 충돌 발생

  • 병합을 진행하게 되면 충돌(conflict) 이 발생하였다고 사용자에게 메시지로 알려줍니다.

6. 충돌이 일어났을 경우 SourceTree 그래프 모양

  • 소스트리를 이용하여 충돌이 일어나게 되면, 회색으로 그래프 라인이 표시가 됩니다.

7. 사용자가 직접 판단하여 내용 수정

  • 충돌이 일어났으므로 깃은 사용자에게 수정할 권할을 위임합니다.
  • 깃은 충돌이 일어나면 자동으로 수정하지 않고, 사용자에게 직접 수정 후 수정이 완료되면 완료된 파일을 기준으로 병합을 진행합니다.

  • <<<<<<< HEAD 이 문구는 master 브랜치에서 작업한 내용을 보여줍니다.
  • >>>>>>> feature 이문구는 feature 브랜치에서 작업한 내용을 보여줍니다.
  • 즉, 깃은 서로 다른 브랜치에서 작업한 내용을 같이 보여주면서, 개발자에게 직접 해당 부분을 수정하고 다시 커밋하라고 말해줍니다.

8. Git 에게 충돌 해결했다고 보고

  • 사용자는 수정을 완료했다면, 다시 깃에게 충돌난 부분 수정을 완료했다고 보고해야 합니다.
  • 보고 한 이후에는 다시 커밋을 진행하면 됩니다.

9. master 브랜치에서 최종적으로 커밋 완료 및 충돌 해결

  • 최종적으로 충돌을 해결하고 커밋을 완료하였다면, 소스트리에서 graph 모양이 변경된 것을 확인하실 수 있습니다.

브랜치 병합 여부 확인

  • 다수의 브랜치가 있을 때는 어느 브랜치가 병합을 완료한 것인지 알기 어렵습니다.
  • 브랜치를 병합한 후에 바로 병합된 브랜치를 삭제한다면 이러한 혼동을 줄일 수 있을 것입니다.
  • 깃은 병합한 브랜치와 병합하지 않은 브랜치를 구분하는 옵션을 제공합니다.
  • 병합한 브랜치는 별표(*) 기호로 표시됩니다.
  • 병합을 완료한 브랜치는 앞에서 배운 -d 옵션을 사용하여 삭제할 수 있습니다.
  • 병합하지 않은 브랜치는 -no-merged 옵션으로 확인할 수 있습니다.
bh.cho@DESKTOP-HIP8HFB MINGW64 /d/git_test (master)
$ git branch --merged
  feature
* master

bh.cho@DESKTOP-HIP8HFB MINGW64 /d/git_test (master)
$ git branch --no-merged

리베이스(rebase)

  • 브랜치를 합치는 방법은 두 가지입니다.
  • 앞에서 배운 병합(merge) 과 이 절에서 학습할 리베이스(rebase) 입니다.
  • 이번에는 커밋 순서를 재배열하는 리베이스 병합을 알아보겠습니다.
  • 리베이스는 커밋의 트리 구조를 재배열합니다.
  • 커밋을 재배열하는 변경 결과가 병합과 유사합니다. 사실 실무에서는 merge 명령어보다는 커밋을 재배열하는 리베이스를 더 선호하는 편입니다.

베이스(base)

  • 모든 브랜치는 뿌리가 있습니다.(master 브랜치는 예외입니다.)
  • 브랜치는 특정 커밋을 가리키는 포인터입니다.
  • 그리고 가리키는 특정 커밋은 브랜치가 파생된 기준이 됩니다.
  • 즉, 브랜치는 커밋 하나를 기준으로 새로운 작업을 진행할 수 있는 분리된 작업 경로를 의미합니다.
  • 아래 그림을 보시게 되면, 커밋2 에서 새로운 브랜치가 파생됩니다.
  • 새로운 브랜치가 파생되는 커밋2를 베이스(base) 라고 합니다.
  • 이를 공통 조상 커밋 이라고 합니다.

베이스(base) 변경

  • 리베이스(rebase) 는 베이스 앞에 '다시' 를 의미하는 re가 붙은 단어입니다.
  • 파생된 브랜치의 기준이 되는 베이스 커밋을 변경하는 것입니다.
  • 브랜치의 베이스를 변경하는 이유는 커밋의 진행 모습을 단순화 하기 위해서 입니다.
  • 브랜치가 많아지면 커밋을 관리하고 파악하기 어렵습니다. 마치 꼬여있는 기찻길처럼 단계별로 커밋을 따라가면서 코드를 확인해야 합니다.
  • 복잡한 브랜치의 수많은 경로는 코드의 진행 상황을 한눈에 파악하기 어렵습니다.
  • 리베이스는 코드의 베이스 분기점을 변경하여 마치 하나의 기찻길처럼 만듭니다.
  • 여러 갈래로 갈라지지 않아 커밋의 진행 사항을 좀 더 쉽게 파악할 수 있습니다.

  • 그림처럼 리베이스는 브랜치 A의 공통된 조상인 커밋2를 master 브랜치의 마지막 커밋6으로 변경합니다.
  • 그리고 모든 브랜치의 커밋들을 리베이스된 커밋6 이후로 재정렬합니다.

리베이스 vs 병합

  • 병합은 파생된 두 브랜치를 하나로 합치는 과정입니다.
  • 병합하려면 두 브랜치의 공통 조상 커밋을 먼저 찾아야 합니다.
  • 공통 조상 커밋을 찾으면 서로 다르게 커밋이 진행된 두 브랜치를 3-way 방식으로 병합할 수 있습니다.
  • 공통 조상 커밋은 두 브랜치를 병합하는 베이스 커밋입니다.
  • 병합하는 두 브랜치는 순차적으로 커밋을 비교하면서 마지막 최종 커밋을 생성합니다.
  • 반면에 리베이스(rebase) 는 두 브랜치를 서로 비교하지 않고 순차적으로 커밋 병합을 시도합니다.

리베이스 + 병합 실습

1. master 브랜치에서 코드 수정 후 commit

  • master 브랜치에서 checkout 후 코드 수정 후 해당 내용을 commit 합니다.

2. feature 브랜치 생성 후 commit

  • feature 브랜치 생성 후, 소스 코드를 수정후 해당 내용을 commit 합니다.

3. master 브랜치로 Checkout 후 다시 커밋

  • feature 브랜치에서 커밋이 완료되었다면, master 브랜치로 다시 checkout 합니다.
  • 그리고 index.html 소스코드를 다시 수정 후 커밋합니다.
  • 현재 그래프 상황은 다음과 같습니다.

4. 다시 feature 로 checkout 하고 베이스로 삼기 원하는 브랜치 설정

  • master 브랜치에서 다시 featrue 브랜치로 checkout 합니다.
  • 그리고 베이스로 삼기 원하는 브랜치(master)에서 우측클릭 -> Rebase -> OK 과정을 진행합니다.

5. 충돌 발생

  • 4번에서 rebase 작업을 진행하면 conflict 가 발생합니다.

6. Uncommited changes에서 Conflict 확인

  • Uncommitted chanegd 에서 어떤 부분이 충돌났는지 확인합니다.

7. 충돌 내용 확인

  • 충돌이 일어난 파일을 열고 어떤 내용들이 충돌났는지 확인합니다.

8. 충돌 내용 수정

  • 개발자가 원하는 방향으로 충돌이 일어난 내용을 수정합니다.

9. 충돌 해결이 끝나면 Staged 이동 및 커밋

  • 8번에서 충돌 내용 수정을 완료하였다면, 해당 파일을 깃에게 충돌 수정을 보고하고 staged 영역으로 이동시킵니다.
  • staged 영역으로 이동되었다면 commit 합니다.

10. Continue Rebase 진행

  • 9번 작업을 완료하였다면, Menu -> Actions -> Continue Rebase 작업을 진행합니다.

11. 리베이스 완료

  • 리베이스 완료된 그래프 모습입니다.
  • feature 브랜치가 분기가 되었던 모습에서 rebase 적용한 후, 다시 master 브랜치 바로 위에 위치해 있는 것을 보실 수 있습니다.

리베이스 할 때 주의할 점

  • 리베이스는 커밋 위치와 해시 값을 변경합니다.
  • 저장소를 외부에 공개했다면 공개된 순간부터 커밋은 리베이스를 사용하지 않는 것이 원칙입니다.
  • 리베이스는 외부로 코드를 푸시하거나 공개하기 전에 로컬에서만 실행하는 것이 좋습니다.
  • 외부에 공개된 커밋을 리베이스하면 커밋 위치와 해시 값이 변경되어 너무 혼란스럽습니다.
728x90

'버전관리' 카테고리의 다른 글

[Git] 10장 배포 관리와 태그  (0) 2022.06.24
[Git] 9장 복귀  (0) 2022.06.20
[Git] 7장 임시 처리  (0) 2022.06.20
[Git] 6장 브랜치  (0) 2022.06.19
[Git] 5장 서버  (0) 2022.06.19

이 글을 공유하기

댓글

Designed by JB FACTORY