18  충돌

사람들이 병렬로 작업을 할 수 있게 됨에 따라, 누군가 다른 사람 작업 영역에 발을 들여 넣을 가능성이 생겼다. 혼자서 작업할 경우에도 이런 현상이 발생한다. 만약 개인 노트북과 연구실 서버에서 소프트웨어 개발을 한다면, 각 작업본에 다른 변경 사항을 만들 수 있다. 버전 제어(version control)는 겹치는 변경 사항을 해결(resolve)하는 도구를 제공함으로써 이러한 충돌(conflicts)을 관리할 수 있게 돕는다.

충돌을 어떻게 해소할 수 있는지 확인하기 위해서 먼저 파일을 하나 생성하자. mars.txt 파일은 현재 두 협업하는 사람의 planets 저장소 사본에서 다음과 같이 보인다.

$ cat mars.txt

Cold and dry, but everything is my favorite color
The two moons may be a problem for Wolfman
But the Mummy will appreciate the lack of humidity

파트너 사본에만 한 줄을 추가하자.

$ nano mars.txt
$ cat mars.txt

Cold and dry, but everything is my favorite color
The two moons may be a problem for Wolfman
But the Mummy will appreciate the lack of humidity
This line added to Wolfman's copy

그리고 나서 변경 사항을 GitHub에 푸시하자.

$ git add mars.txt
$ git commit -m "Add a line in our home copy"

[main 5ae9631] Add a line in our home copy
 1 file changed, 1 insertion(+)
$ git push origin main

Counting objects: 5, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 352 bytes, done.
Total 3 (delta 1), reused 0 (delta 0)
To https://github.com/vlad/planets
   29aba7c..dabb4c8  main -> main

이제 다른 파트너가 GitHub에서 갱신(update)하지 않고 본인 사본에 다른 변경 사항을 작업한다.

$ nano mars.txt
$ cat mars.txt

Cold and dry, but everything is my favorite color
The two moons may be a problem for Wolfman
But the Mummy will appreciate the lack of humidity
We added a different line in the other copy

로컬 저장소에 변경 사항을 커밋할 수 있다.

$ git add mars.txt
$ git commit -m "Add a line in my copy"

[main 07ebc69] Add a line in my copy
 1 file changed, 1 insertion(+)

하지만 Git이 GitHub에는 푸시할 수 없게 한다.

$ git push origin main

To https://github.com/vlad/planets.git
 ! [rejected]        main -> main (non-fast-forward)
error: failed to push some refs to 'https://github.com/vlad/planets.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Merge the remote changes (e.g. 'git pull')
hint: before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
그림 18.1: 충돌하는 변경사항

Git이 푸시를 거절한다. 이유는 로컬 브랜치로 반영되지 않는 새로운 업데이트가 원격 저장소에 있음을 Git이 탐지했기 때문이다. 즉, 본인이 작업한 변경 사항이 다른 사람이 작업한 변경 사항과 중첩되는 것을 Git이 탐지해서 앞에서 작업한 것을 덮어쓰지 않도록 정지시킨다. 이제 해야 할 작업은 GitHub에서 변경 사항을 풀(Pull)해서 가져오고 현재 작업 중인 작업본과 병합(merge)해서 푸시한다. 풀(Pull)부터 시작하자.

$ git pull origin main

remote: Counting objects: 5, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 1), reused 3 (delta 1)
Unpacking objects: 100% (3/3), done.
From https://github.com/vlad/planets
 * branch            main     -> FETCH_HEAD
Auto-merging mars.txt
CONFLICT (content): Merge conflict in mars.txt
Automatic merge failed; fix conflicts and then commit the result.

git pull 명령어는 로컬 저장소를 갱신할 때 원격 저장소에 이미 반영된 변경 사항을 포함시키도록 한다. 원격 저장소 브랜치에서 변경 사항을 가져온(fetch) 후에 로컬 저장소 사본의 변경 사항이 원격 저장소 사본과 겹치는 것을 탐지해냈다. 따라서 앞서 작업한 것이 덮어쓰지 않도록 서로 다른 두 버전의 병합(merge)을 승인하지 않고 거절한 것이다. 해당 파일에 충돌나는 부분을 다음과 같이 표시해 놓는다.

$ cat mars.txt

Cold and dry, but everything is my favorite color
The two moons may be a problem for Wolfman
But the Mummy will appreciate the lack of humidity
<<<<<<< HEAD
We added a different line in the other copy
=======
This line added to Wolfman's copy
>>>>>>> dabb4c8c450e8475aee9b14b4383acc99f42af1d

<<<<<<< HEAD로 시작되는 부분에 본인 변경 사항이 나와 있다. Git이 자동으로 =======를 넣어 충돌나는 변경 사항 사이에 구분자로 넣고, >>>>>>>기호는 GitHub에서 다운로드된 파일 내용의 마지막을 표시한다. (>>>>>>>표시자 다음에 문자와 숫자로 구성된 문자열은 방금 다운로드한 커밋 번호의 식별자다.)

파일을 편집해서 표시자/구분자를 제거하고 변경 사항을 일치시키는 것은 전적으로 여러분에게 달려 있다. 원하는 것이면 무엇이든 할 수 있다. 예를 들어 로컬 저장소의 변경 사항을 반영하든, 원격 저장소의 변경 사항을 반영하든, 로컬과 원격 저장소의 내용을 대체하는 새로운 것을 작성하든, 혹은 변경 사항을 완전히 제거하는 것도 가능하다. 로컬과 원격 모두 교체해서 다음과 같이 파일이 보이도록 하자.

$ cat mars.txt

Cold and dry, but everything is my favorite color
The two moons may be a problem for Wolfman
But the Mummy will appreciate the lack of humidity
We removed the conflict on this line

병합을 마무리하기 위해 병합으로 생성된 변경 사항을 mars.txt 파일에 추가하고 커밋한다.

$ git add mars.txt
$ git status

On branch main
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:

    modified:   mars.txt
$ git commit -m "Merge changes from GitHub"

[main 2abf2b1] Merge changes from GitHub

이제 변경 사항을 GitHub에 푸시할 수 있다.

$ git push origin main

Counting objects: 10, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 697 bytes, done.
Total 6 (delta 2), reused 0 (delta 0)
To https://github.com/vlad/planets.git
   dabb4c8..2abf2b1  main -> main

Git이 병합하면서 수행한 것을 모두 추적하고 있어서 수작업으로 다시 고칠 필요는 없다. 처음 변경 사항을 만든 협력자 프로그래머가 다시 풀하게 되면 다음과 같은 결과를 얻는다.

$ git pull origin main

remote: Counting objects: 10, done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 6 (delta 2), reused 6 (delta 2)
Unpacking objects: 100% (6/6), done.
From https://github.com/vlad/planets
 * branch            main     -> FETCH_HEAD
Updating dabb4c8..2abf2b1
Fast-forward
 mars.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

병합된 파일을 얻게 된다.

$ cat mars.txt

Cold and dry, but everything is my favorite color
The two moons may be a problem for Wolfman
But the Mummy will appreciate the lack of humidity
We removed the conflict on this line

다시 병합할 필요는 없는데, 다른 누군가 작업을 했다는 것을 Git이 알기 때문이다.

충돌을 해소하는 Git 기능은 매우 유용하지만, 충돌 해소에는 시간과 노력이 수반되고, 충돌이 올바르게 해소되지 않으면 오류가 스며들게 된다. 프로젝트 와중에 상당량의 충돌을 해소하는 데 시간을 쓰고 있다고 생각되면, 충돌을 줄일 수 있는 기술적인 접근법도 고려해 보는 것이 좋다.

좀 더 자주 upstream을 풀(Pull)하기, 특히 새로운 작업을 시작하기 전이라면 더욱 그렇다. 작업을 구별하기 위해 토픽 브랜치를 사용해서 작업을 완료하면 메인(main) 브랜치에 병합시킨다. 좀 더 작게 원자 수준 커밋을 한다. 논리적으로 적절하다면 큰 파일을 좀 더 작은 것으로 쪼갠다. 그렇게 함으로써 두 저자가 동시에 동일한 파일을 변경하는 것을 줄일 수 있을 듯 싶다. 프로젝트 관리 전략으로 충돌을 최소화할 수도 있다.

프로젝트 관리 전략으로 충돌을 최소화할 수도 있다.

본인이 생성한 충돌 해소하기

강사가 생성한 저장소를 복제하세요. 저장소에 새 파일을 추가하고 기존 파일을 변경하세요. (강사가 변경할 기존 파일이 어느 것인지 알려줄 것이다.) 강사의 말에 따라 충돌을 생성하는 연습을 위해 저장소에서 변경 사항을 가져오도록 풀(Pull)하세요. 그리고 충돌을 해소하고 해결해 보세요.

텍스트 파일이 아닌 충돌

버전 제어 저장소의 이미지 파일이나 혹은 다른 텍스트가 아닌 파일에서 충돌이 발생할 때 Git은 무엇을 하나요?

먼저 시도해 보자. 드라큘라가 화성 표면에서 사진을 찍어 mars.jpg로 저장했다고 가정한다.

화성 이미지 파일이 없다면 다음과 같이 더미 바이너리 파일을 생성할 수도 있다.

$ head --bytes 1024 /dev/urandom > mars.jpg
$ ls -lh mars.jpg

-rw-r--r-- 1 vlad 57095 1.0K Mar  8 20:24 mars.jpg

ls 명령어를 사용해서 파일 크기가 1 킬로바이트임이 확인된다. /dev/urandom 특수 파일에서 불러온 임의 바이트로 꽉 차있다.

이제, 드라큘라가 mars.jpg 파일을 본인 저장소에 저장한다고 상정한다:

$ git add mars.jpg
$ git commit -m "Add picture of Martian surface"

[main 8e4115c] Add picture of Martian surface
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 mars.jpg

늑대인간도 비슷한 시점에 유사한 사진을 추가했다고 가정한다. 늑대인간의 사진은 화성 하늘 사진인데, 이름도 mars.jpg로 동일하다. 드라큘라가 푸시하게 되면 유사한 메시지를 받게 된다.

$ git push origin main

To https://github.com/vlad/planets.git
! [rejected]        main -> main (fetch first)
error: failed to push some refs to 'https://github.com/vlad/planets.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

풀을 먼저 한 뒤에 충돌나는 것을 해소한다는 것을 학습했다.

$ git pull origin main

이미지나 기타 바이너리 파일에 충돌이 생길 때, Git은 다음과 같은 메시지를 출력한다:

$ git pull origin main
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From https://github.com/vlad/planets.git
* branch            main     -> FETCH_HEAD
  6a67967..439dc8c  main     -> origin/main
warning: Cannot merge binary files: mars.jpg (HEAD vs. 439dc8c08869c342438f6dc4a2b615b05b93c76e)
Auto-merging mars.jpg
CONFLICT (add/add): Merge conflict in mars.jpg
Automatic merge failed; fix conflicts and then commit the result.

이번에도 충돌 메시지가 mars.txt에 나온 것과 거의 동일하다. 하지만 중요한 추가 라인 한 줄이 있다.

warning: Cannot merge binary files: mars.jpg (HEAD vs. 439dc8c08869c342438f6dc4a2b615b05b93c76e)

Git은 자동으로 텍스트 파일에 했던 것처럼 이미지 파일에 충돌 지점 표식을 끼워 넣을 수 없다. 그래서 이미지 파일을 편집하는 대신에 간직하고자 하는 버전을 체크아웃(checkout)하고 나서 해당 버전을 추가(add)하고 커밋한다.

중요한 라인에 mars.jpg의 두 가지 버전에 대해 커밋 식별자(commit identifier)를 Git이 제시하고 있다. 현재 작업 버전은 HEAD이고, 늑대인간 작업 버전은 439dc8c0...이다. 본인 작업 버전을 사용하고자 하면 git checkout 명령어를 사용한다.

$ git checkout HEAD mars.jpg
$ git add mars.jpg
$ git commit -m "Use image of surface instead of sky"

[main 21032c3] Use image of surface instead of sky

대신에 늑대인간 버전을 사용하려고 하면 git checkout 명령어를 늑대인간 439dc8c0 커밋 식별자와 함께 사용하면 된다.

$ git checkout 439dc8c0 mars.jpg
$ git add mars.jpg
$ git commit -m "Use image of sky instead of surface"

[main da21b34] Use image of sky instead of surface

이미지 모두 보관할 수도 있다. 동일한 이미지명으로 보관할 수는 없다는 것이 중요하다. 순차적으로 각 버전을 체크아웃(checkout)하고 나서 이미지명을 변경한다. 그리고 나서 이름을 변경한 버전을 추가한다. 먼저 각 이미지를 체크아웃하고 이름을 변경하자.

$ git checkout HEAD mars.jpg
$ git mv mars.jpg mars-surface.jpg
$ git checkout 439dc8c0 mars.jpg
$ mv mars.jpg mars-sky.jpg

그리고 나서 mars.jpg 이전 파일을 삭제하고 새로운 파일 두 개를 추가한다.

$ git rm mars.jpg
$ git add mars-surface.jpg
$ git add mars-sky.jpg
$ git commit -m "Use two images: surface and sky"

[main 94ae08c] Use two images: surface and sky
2 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 mars-sky.jpg
rename mars.jpg => mars-surface.jpg (100%)

이제 화성 이미지 파일 두 개가 저장소에서 확인되지만 mars.jpg 파일은 더 이상 존재하지 않는다.

일반적인 작업 시간

원격 Git 저장소를 활용하여 공동 프로젝트로 작업하는 컴퓨터 앞에 앉아 있다. 작업 시간 동안에 다음 동작을 취하지만 작업 순서는 다르다.

  • 변경한다(make change): numbers.txt 텍스트 파일에 숫자 100을 추가.
  • 원격 저장소 갱신시키기(Update remote): 로컬 저장소와 매칭되어 동기화시킴.
  • 축하하기(Celebrate): 맥주로 성공을 자축함.
  • 로컬 저장소 갱신시키기(Update local): 원격 저장소와 매칭되어 동기화시킴.
  • 변경 사항 준비영역으로 보내기(Stage change): 커밋 대상으로 추가하기.
  • 변경 사항 커밋하기(Commit change): 로컬 저장소에 커밋하기.

어떤 순서로 작업을 수행해야 충돌이 날 가능성을 최소화할 수 있을까? 아래 표의 action 칼럼에 순서대로 상기 명령어를 적어 본다.

작업 순서를 정했으면 command 칼럼에 대응되는 명령어를 적어 본다. 일부 단계를 시작하는 데 도움이 되도록 채워져 있다.

order action . . . . . . . . . . command . . . . . . . . . .
1
2 echo 100 >> numbers.txt
3
4
5
6 Celebrate! AFK
order action . . . . . . command . . . . . . . . . . . . . . . . . . .
1 Update local git pull origin main
2 Make changes echo 100 >> numbers.txt
3 Stage changes git add numbers.txt
4 Commit changes git commit -m "Add 100 to numbers.txt"
5 Update remote git push origin main
6 Celebrate! AFK 1

  1. AFK는 “Away From Keyboard”의 약자로 사용자가 컴퓨터 앞에 있지 않다는 것을 나타내는 데 사용된다. 주로 게임이나 채팅 중에 자리를 비울 때 다른 사용자들에게 알리기 위해 사용된다.↩︎