본문 바로가기

프로그래밍 독학/Git

Pro git 독학일지 5장 - Git 브랜치(1)(Branch, Merge)

반응형

Pro git 독학일지 5장 - Git 브랜치(1)(Branch, Merge)

 

모든 버전관리 시스템은 브랜치를 지원한다.

개발을 하다 보면 코드를 여러 개로 복사해야 하는 일이 자주 생긴다.

코드를 통째로 복사하고 나서 원래 코드와는 상관없이 독립적으로 개발을 진행할 수 있는데,

이렇게 독립적으로 개발하는 것이 브랜치다.

 

사람들은 브랜치 모델이 Git의 최고의 장점이라고, Git이 다른 것들과 구분되는 특징이라고 말한다.

당최 어떤 점이 그렇게 특별한 것일까?

Git의 브랜치는 매우 가볍다.

순식간에 브랜치를 새로 만들고 브랜치 사이를 이동할 수 있다.

다른 버전관리 시스템과는 달리 Git은 브랜치를 만들어 작업하고 나중에 Merge하는 방법을 권장한다

심지어 하루에 수십번씩 해도 괜찮다.

Git 브랜치에 능숙해지면 개발 방식이 완전히 바뀌고 다른 도구를 사용할 수 없게 된다.

 

 

브랜치란 무엇인가

 

Git이 브랜치를 다루는 과정을 이해하려면 우선 Git이 데이터를 어떻게 저장하는지 알아야 한다.

 

Git은 데이터를 일련의 스냅샷으로 기록한다.

 

커밋하면 Git은

1. 현 Staging Area에 있는 데이터 스냅샷에 대한 포인터,

2. 저자나 커밋 메시지 같은 메타데이터,

3. 이전 커밋에 대한 포인터 등을 포함하는 커밋 개체(커밋 Object)

를 저장한다.

이전 커밋 포인터가 있어서 현재 커밋이 무엇을 기준으로 바뀌었는 지를 알 수 있다.

최초 커밋을 제외한 나머지 커밋은 이전 커밋 포인터가 적어도 하나씩 있다.

브랜치를 합친 Merge 커밋 같은 경우에는 이전 커밋 포인터가 여러 개 있다.

 

파일이 3개 있는 디렉토리가 하나 있고 이 파일을 Staging Area에 저장하고 커밋하는 예제를 살펴보자.

파일을 Stage하면 Git 저장소에 파일을 저장(Blob)하고 Staging Area에 해당 파일의 체크섬을 저장한다.

 

git commit으로 커밋하면 먼저 루트 디렉토리와 각 하위 디렉토리의 트리 개체를 체크섬과 함께 저장소에 저장한다.

그 다음에 커밋 개체를 만들고 메타데이터와 루트 디렉토리 트리 개체를 가리키는 포인터 정보를 커밋 개체에 넣어 저장한다.

그래서 필요하면 언제든지 스냅샷을 다시 만들 수 있다.

 

이 작업을 마치고 나면 Git 저장소에는 다섯개의 데이터 개체가 생긴다. 

각 파일에 대한 Blob 세 개,

파일과 디렉토리 구조가 들어있는 트리 개체 하나,

메타데이터와 루트 트리를 가리키는 포인터가 담긴 커밋 개체 하나이다.

 

 

Git의 브랜치는 커밋 사이를 가볍게 이동할 수 있는 어떤 포인터 같은 것이다.

기본적으로 Git은 master 브랜치를 만든다.

처음 커밋하면 이 master 브랜치가 생성된 커밋을 가리킨다.

이후 커밋을 만들면 브랜치는 자동으로 가장 마지막 커밋을 가리킨다.

 

 

새 브랜치 생성하기

 

브랜치를 하나 만들어서 놀자. 아래와 같은 명령으로 testing 브랜치를 만든다.

 

$ git branch testing

 

새로 만든 브랜치도 지금 작업하고 있던 마지막 커밋을 가리킨다.

 

 

지금 작업 중인 브랜치가 무엇인지 Git은 어떻게 파악할까?

다른 버전관리 시스템과는 달리 Git은 'HEAD'라는 특수한 포인터가 있다. 

이 포인터는 지금 작업하는 로컬 브랜치를 가리킨다.

브랜치를 새로 만들었지만, Git은 아직 master 브랜치를 가리키고 있다.

git branch 명령은 브랜치를 만들기만 하고 브랜치를 옮기지 않는다.

 

 

git log 명에 --decorate 옵션을 사용하면 쉽게 브랜치가 어떤 커밋을 가리키는지도 확인할 수 있다.

 

 

master와 testing이라는 브랜치가 f30ab 커밋 옆에 위치하여 이런식으로 브랜치가 가리키는 커밋을 확인할 수 있다.

 

 

브랜치 이동하기

 

git checkout 명령으로 다른 브랜치로 이동할 수 있다.

 

$ git checkout testing

 

이렇게 하면 HEAD는 testing 브랜치를 가리킨다.

 

 

 

 

이 부분이 흥미롭다. 새로 커밋해서 testing 브랜치는 앞으로 이동했다.

하지만 master 브랜치는 여전히 이전 커밋을 가리킨다. master 브랜치로 되돌아가보자

 

 

방금 실행한 명령이 한 일은 두 가지다.

1. master 브랜치가 가리키는 커밋을 HEAD가 가리키게 하고

2. 워킹 디렉토리의 파일도 그 시점으로 되돌려 놓았다.

 

앞으로 커밋을 하면 다른 브랜치의 작업들과 별개로 진행되기 때문에

testing 브랜치에서 임시로 작업하고 원래 master 브랜치로 돌아와서 하던 일을 계속할 수 있다.

 

브랜치를 이동하면 워킹 디렉토리의 파일이 변경된다는 점을 기억해두어야 한다.

이전에 작업했던 브랜치로 이동하면 워킹 디렉토리의 파일은 그 브랜치에서 가장 마지막으로 했던 작업 내용으로 변경된다.

파일 변경시 문제가 있어 브랜치를 이동시키는게 불가능한 경우 Git은 브랜치 이동 명령을 수행하지 않는다.

 

파일을 수정하고 다시 커밋을 해보자.

 

 

프로젝트 히스토리는 분리돼 진행한다.

우리는 브랜치를 하나 만들어 그 브랜치에서 일을 좀 하고, 다시 원래 브랜치로 되돌아와서 다른 일을 했다.

두 작업 내용은 서로 독립적으로 각 브랜치에 존재한다.

커밋 사이를 자유롭게 이동하다가 때가 되면 두 브랜치를 Merge한다.

간단히 branch, checkout, commit 명령을 써서 말이다.

 

git 로그 명령은 현재 브랜치가 가리키고 있는 히스토리가 무엇이고 어떻게 갈라져 나왔는지 보여준다.

git log --oneline --decorate --graph --all 

이라고 실행하면 히스토리를 출력한다.

 

 

실제로 Git의 브랜치는 어떤 한 커밋을 가리키는 40글자의 SHA-1 체크섬 파일에 불과하기 때문에 만들기도 쉽고 지우기도 쉽다.

새로 브랜치를 하나 만드는 것은 41바이트 크기의 파일을 하나 만드는 것에 불과하다.

 

 

브랜치와 Merge의 기초

 

실제 개발과정에서 겪을만한 에제를 하나 살펴보자.

브랜치와 Merge는 보통 이런 식으로 진행한다.

 

1. 작업중인 웹사이트가 있다.

2. 새로운 이슈를 처리할 새 브랜치를 하나 생성한다.

3. 새로 만든 브랜치에서 작업을 진행한다.

 

이 때 중요한 문제가 생겨서 그것을 해결하는 Hotfix를 먼저 만들어야 한다. 

그러면 아래와 같이 할 수 있다.

 

1. 새로운 이슈를 처리하기 이전의 운영(Production) 브랜치로 이동한다.

2. Hotfix 브랜치를 새로 하나 생성한다.

3. 수정한 Hotfix 테스트를 마치고 운영 브랜치로 Merge한다.

4. 다시 작업하던 브랜치로 옮겨가서 하던 일 마저 한다.

 

 

브랜치의 기초

 

먼저 지금 작업하는 프로젝트에서 이전에 커밋을 몇 번 했다고 가정한다.

이슈관리 시스템에 등록된 53번 이슈를 처리한다고 하면 이 이슈에 집중할 수 있는 브랜치를 새로 하나 만든다.

브랜치를 만들면서 Checkout까지 한번에 하려면 git checkout 명령에 -b 라는 옵션을 추가한다.

 

 

iss53 브랜치를 Checkout 했기 때문에 HEAD는 iss53 브랜치를 가리킨다.

뭔가 일을 하고 커밋하면 iss53 브랜치가 앞으로 나아간다.

 

다른 상황을 가정해보자.

만드는 사이트에 문제가 생겨서 즉시 고쳐야 한다.

버그를 해결한 Hotfix에 iss53이 섞이는 것을 방지하기 위해

iss53과 관련된 코드를 어딘가에 저장해두고 원래 운영 환경의 소스로 복구해야 한다.

master 브랜치로 돌아가면 된다.

 

하지만, 아직 커밋하지 않은 파일이  Checkout할 브랜치와 충돌나면 브랜치를 변경할 수 없다.

브랜치를 변경할 때는 워킹  디렉토리를 정리하는 것이 좋다.

이런 문제를 다루는 방법은 나중에 Stashing과 Cleaning에서 다룰 것이다.

지금은 작업하던 것을 모두 커밋하고 master 브랜치로 옮긴다.

 

 

이 떄 워킹 디렉토리는 53번 이슈를 시작하기 이전 모습으로 되돌려지기 때문에 새로운 문제에 집중할 수 있는 환경이 만들어진다.

Git은 자동으로 워킹 디렉토리에 파일들을 추가하고, 지우고, 수정해서

Checkout한 브랜치의 마지막 스냅샷으로 되돌려 놓는다는 것을 기억해야 한다.

 

이젠 해결해야 할 핫픽스가 생겼을 때를 살펴보자.

hotfix라는 브랜치를 만들고 새로운 이슈를 해결할 때까지 사용한다.

 

 

운영환경에 적용하려면 문제를 제대로 고쳤는지 테스트하고 master 브랜치에 합쳐야 한다.

git merge 명령으로 아래와 같이 한다.

 

 

Merge 메시지에서 'fast forward'가 보이는가.

hotfix 브랜치가 가리키는 C4 커밋이 C2 커밋에 기반한 브랜치이기 때문에

브랜치 포인터는 Merge 과정 없이 그저 최신 커밋으로 이동한다.

 

이런 Merge 방식을 Fast forward 라고 부른다.

다시 말해 A 브랜치에서 다른 B 브랜치를 Merge할 때 B 브랜치가 A 브랜치 이후의 커밋을 가리키고 있으면

그저 A 브랜치가 B 브랜치와 동일한 커밋을 가리키도록 이동시킬 뿐이다.

 

이제 hotfix는 master 브랜치에 포함됐고 운영환경에 적용할 수 있는 상태가 되었다고 가정해보자.

 

 

급한 문제를 해결하고 master 브랜치에 적용하고 나면 다시 일하던 브랜치로 돌아가야 한다.

이제 더이상 필요없는 hotfix 브랜치는 삭제한다.

git branch 명령에 -d 옵션을 주고 브랜치를 삭제한다.

 

 

위에서 작업한 hotfix가 iss53 브랜치에 영향을 끼치지 않는다는 점을 이해하는 것이 중요하다.

git merge master 명령으로 master 브랜치를 iss53 브랜치에 Merge하면 iss53 브랜치에 hotfix가 적용된다.

아니면 iss53 브랜치가 master에 Merge할 수 있는 수준이 될 때까지 기다렸다가 Merge하면 hotfix와 iss53 브랜치가 합쳐진다.

 

 

Merge의 기초

 

53번 이슈를 다 구현하고 master 브랜치에 Merge 하는 과정을 살펴보자.

iss53 브랜치를 master 브랜치에 Merge하는 것은 앞서 살펴본 hotfix 브랜치를 Merge하는 것과 비슷하다.

git merge 명령으로 합칠 브랜치에서 합쳐질 브랜치를 Merge하면 된다.

 

 

hotfix를 Merge했을 때와 메시지가 다르다.

현재 브랜치가 가리키는 커밋이 Merge할 브랜치의 조상이 아니므로 Git은 Fast-forward로 Merge하지 않는다.

이 경우에 Git은 각 브랜치가 가리키는 커밋 두 개와 공통 조상 하나를 사용하여 3-way-merge를 한다.

 

 

단순히 브랜치 포인터를 최신 커밋으로 옮기는게 아니라 3-way Merge 의 결과를 별도의 커밋으로 만들고 나서

해당 브랜치가 그 커밋을 가리키도록 이동시킨다.

그래서 이런 커밋은 부모가 여러 개고 Merge 커밋이라고 부른다.

 

 

Git은 Merge하는 데 필요한 최적의 공통 조상을 자동으로 찾는다.

iss53 브랜치를 master에 Merge하고 나면 더는 iss53 브랜치가 필요 없다.

다음 명령으로 브랜치를 삭제하고 이슈의 상태를 처리 완료로 표시한다.

 

$ git branch -d iss53

 

 

충돌의 기초

 

가끔씩 3-way Merge가 실패할 때도 있다.

Merge하는 두 브랜치에서 같은 파일의 한 부분을 동시에 수정하고 Merge하면 Git은 해당 부분을 Merge하지 못한다.

예를 들어, 53번 이슈와 hotfix가 같은 부분을 수정했다면

Git은 Merge 하지 못하고 아래와 같은 충돌(Conflict) 메시지를 출력한다.

 

 

Git은 자동으로 Merge하지 못해서 새 커밋이 생기지 않는다.

변경사항의 충돌을 개발자가 해결하지 않는 한 Merge과정을 진행할 수 없다.

Merge 충돌이 일어났을 떄 Git이 어떤 파일을 Merge할 수 없었는지 살펴보려면 git status 명령을 이용한다.

 

 

충돌이 일어난 파일은 unmerged 상태로 표시된다. 

Git은 충돌이 난 부분을 표준 형식에 따라 표시해준다.

그러면 개발자는 해당 부분을 수동으로 해결한다.

충돌난 부분은 아래와 같이 표시된다.

 

 

======= 위쪽의 내용은 HEAD 버전(merge 명령을 실행할 떄 작업하던 master 브랜치)의 내용이고

아래쪽은 iss53 브랜치의 내용이다.

충돌을 해결하려면 위쪽이나 아래쪽 내용 중에서 고르거나 새로 작성하여 Merge한다.

아래는 아예 새로 작성하여 충돌을 해결하는 예제다.

 

 

충돌한 양쪽에서 조금씩 가져와서 새로 수정했다.

그리고 <<<<<<<, =======, >>>>>>> 가 포함된 행을 삭제했다.

다른 Merge 도구도 충돌을 해결할 수 있다.

 

git mergetool 명령으로 실행한다

 

기본 도구 말고 사용할 수 있는 다른 Merge 도구도 있는데

Mac에서는 opendiff가 실행된다.

'one of the following tools' 부분에 보여준다.

여기에 표시된 도구 중 하나를 고를 수 있다.

 

Merge 도구를 종료하면 Git은 잘 Merge했는지 물어본다.

잘 마쳤다고 입력하면 자동으로 git add가 수행되고 해당 파일이 Staging Area에 저장된다.

git status 명령으로 충돌이 해결된 상태인지 다시 한 번 확인해볼 수 있다.

 

충돌을 해결하고 나서 해당 파일이 Staging Area에 저장됐는지 확인했으면 git commit 명령으로 Merge한 것을 커밋한다.

충돌을 해결하고 Merge할 때는 커밋 메시지가 아래와 같다.

 

 

어떻게 충돌을 해결했고 좀 더 확인해야 하는 부분은 무엇이고 왜 그렇게 해결했는지에 대해 자세하게 기록한다.

자세한 기록은 나중에 이 Merge 커밋을 이해하는데 도움을 준다.

 

 

반응형