지난 시간에는 Remote Repository에서의 브랜치, Local Repository에서 브랜치를 push하는 방법 등에 대해 함께 배웠습니다.

이번 시간에는 git reset과 브랜치 간의 관련성, git reset과 git checkout 커맨드 간의 차이점, 그리고 새로운 커밋을 만들지 않는 merge에 대한 내용을 심화적으로 학습해보겠습니다.

🌿 git reset의 비밀

브랜치와 HEAD가 포인터라는 사실을 알면 우리가 앞서 배운 git reset 커맨드의 동작 원리를 더욱 자세히 알 수 있는데요.

❗ git reset 시, HEAD의 변화

HEAD와 master 브랜치가 최신 커밋을 가리키고 있는 상황에서 git reset을 하면 어떻게 될까요? 그럼 master 브랜치는 이전의 커밋을 가리키게 되고 HEAD는 여전히 master 브랜치를 가리키면서 결과적으로는 이전의 커밋을 가리키게 됩니다.

이제 git reset을 했을 때, HEAD가 가리키던 커밋이 바뀐다는 말이 정확히 무슨 뜻인지 아시겠죠? 위와 같은 원리가 적용되고 있던 것이었습니다. 그런 다음, 이전에 배운 각 옵션(soft/mixed/hard)에 따라 과거의 커밋처럼 working direcotry나 staging area의 내용이 리셋되는지가 결정되는 것이구요.

❗ git reset 이후, 최신 커밋은?

한 가지 더 알아야 할 것은 git reset을 한다고 해서 그 이후의 커밋이 삭제되지 않는다는 사실입니다. 또 하나, git reset이라 해서 무조건 이전 커밋으로 돌아가는 것이 아니라는 사실도 알아야 합니다.

만약 HEAD가 이전 커밋을 가리키고 있는 상태에서 git reset 뒤에 최신 커밋의 아이디를 적으면 master 브랜치가 다시 최신 커밋을 가리키게 됩니다.

이 두 사실을 알고 있으면 앞으로 git reset을 사용해서 커밋 사이를 자유자재로 이동할 수 있습니다.

그렇다면 hard 옵션으로 인해 과거 이후의 커밋이 모두 사라진다는 말은 어떻게 된 걸까요? 사실 커밋의 이력이 남아있다면 reflog라는 커맨드를 통해 복원이 가능합니다. 하지만 만약 working directory에 reset --hard로 인해 커밋하지 않은 내용으로 변경되면 커밋하지 않은 내용은 복구할 수 없습니다. 이력이 없기 때문이죠.

중요한 것은 커밋의 이력이 남아있냐, 남아있지 않냐의 차이입니다. 이력이 남아있어야만 복구가 가능하기 때문이죠. 따라서, 평소에 커밋과 커밋의 위치에 대해 신중히 생각하는 습관을 들이시길 바랍니다.

🌿 git reset과 git checkout의 차이

git reset에서 HEAD와 master 브랜치는 함께 움직였습니다. 그런데 HEAD와 master 브랜치는 분리될 수 없는 걸까요? 아닙니다. HEAD 자체가 가리키던 것을 바꿀 수 있기 때문이죠. 심지어 아예 HEAD가 커밋을 직접적으로 가리키게 하는 것도 가능합니다. 바로 git checkout 커맨드를 사용해서 말이죠.

❗ git checkout의 역할

HEAD와 master 브랜치가 최신 커밋을 가리키고 있는 상태에서 git checkout 커맨드를 입력하고 그 옆에 이전 커밋 아이디를 적어주면 master 브랜치는 그대로 최신 커밋을 가리키지만 HEAD이전 커밋을 직접 가리키게 됩니다.

이와 같이 master 브랜치로부터 떨어진 HEAD를 'Detached HEAD'라고 부릅니다. Detached는 우리말로 '~로부터 떨어진, 분리된'이라는 뜻을 가지고 있죠. 이렇게 HEAD가 특정 커밋을 직접 가리키게 하는 이유는 여러 가지가 있는데요. 그 중 중요한 한 가지 이유는 바로 과거의 특정 커밋에서 새로운 브랜치를 만들고 싶기 때문입니다.

Detached HEAD인 상태에서 git branch premium으로 premium 브랜치를 새로 만들면 premium이라는 새로운 브랜치가 생성되고 HEAD와 premium 브랜치가 이전의 커밋을 함께 가리키게 됩니다.

여기서 한 가지 새로운 사실을 알려드리자면, git checkout 커맨드로는 HEAD가 직접 커밋을 가리키게 할뿐만 아니라 브랜치를 직접 가리키게 만들 수도 있습니다.

git checkout premium

위 커맨드를 입력하면 HEAD가 premium 브랜치를 가리키게 됩니다. 따라서, 맨 처음 HEAD가 master 브랜치를 가리킬 때와 마찬가지로 이번에는 HEAD가 premium 브랜치를 가리키게 됩니다. 그럼 더이상 Detached HEAD가 아니고 일반적인 HEAD가 됩니다.

그리고 이 상태에서 새 커밋을 하게 되면, 이제 premium 브랜치로 master 브랜치와 다른 새로운 코드 관리 흐름을 가지게 됩니다. 방금 한 것과 같이 특정 커밋을 시작점으로 하는 새로운 브랜치를 만들고 싶을 때, HEAD를 잠시 Detached HEAD 상태로 두는 경우가 많습니다.

정리하자면, git checkout 커맨드로는 HEAD가 직접적으로 가리키는 것을 바꿀 수 있고 이 커맨드 뒤에는 커밋 아이디 또는 브랜치 이름을 줌으로써 HEAD가 직접 커밋을 가리키거나 브랜치를 가리키도록 할 수 있다는 것입니다.

그런데 사실 git checkout 뒤에 브랜치의 이름이 오는 경우는 이미 우리가 배운 내용입니다. 이는 브랜치를 이동할 때 사용되었죠. 위 내용을 배우고 나니 조금 다르게 보이지 않나요?

git checkout master

사실 이 커맨드의 기능은 premium 브랜치를 가리키고 있던 HEAD가 직접 master 브랜치를 가리키도록 하는 것이었습니다. 이렇게 HEAD가 다른 브랜치가 가리키던 커밋을 가리키게 되면 그에 맞게 working directory의 내용이 바뀌고 그 결과 우리는 브랜치가 변경되었다는 것을 알 수 있게 된 것이었습니다.

즉, 위 커맨드는 master 브랜치로 이동해서 HEAD가 master 브랜치를 가리키도록 하고 master 브랜치가 가리키던 커밋을 간접적으로 가리켜서 working directory의 내용도 그 커밋에 맞게 바꿈으로써 master 브랜치로 이동한 것을 사용자로 하여금 실감하도록 한 것이었습니다.

❗ git reset vs. git checkout

그럼 git reset과 git checkout의 차이점은 뭘까요? 아래의 표를 참고해주세요.

🌿 새로운 커밋 없는 merge

이번에는 merge에 관한 심화 내용을 배워보겠습니다. 앞서, merge를 하면 새로운 커밋이 생긴다고 했었죠? 그리고 이 새로운 커밋을 머지 커밋(merge commit)이라고 부른다고 했습니다.

그런데 merge 커밋이 항상 생기는 것은 아닙니다. master 브랜치와 premium 브랜치가 서로 다른 커밋을 가리키고 있는 상태에서 HEAD가 master 브랜치를 가리키고 있는 상황을 가정해 봅시다. 이때,

git merge premium

을 하게 되면 premium 브랜치가 가리키고 있던 커밋을 HEAD와 master 브랜치가 함께 가리키게 됩니다. 그럼 총 커밋 수는 변하지 않고 단지 브랜치만 이동하게 됩니다. 이러한 merge를 'Fast-foward merge'라고 합니다. 'Fast-foward'는 우리말로 '빨리 감다'라는 뜻을 가지고 있는데요. 위와 같이 master 브랜치가 더 최신 커밋으로 이동하는 모습이 마치 빨리 감기를 하는 모습과 같아 보입니다.

어떤 경우에 이렇게 될까요? 커밋 히스토리에서 같은 선 상에 있는 브랜치를 merge할 때, Fast-foward merge가 이루어집니다. 방금 두 브랜치는 서로 다른 흐름이 아니라 같은 흐름에서 있었기 때문에 같은 선 상에 있다고 볼 수 있습니다.

그러나 기존에 배웠던 merge의 경우, 커밋 히스토리 상에서 분리된 두 개의 코드 관리 흐름에서 진행되었습니다. 이런 상황에서야 merge 커밋이 새롭게 생기는 것입니다. 그리고 이런 merge를 '3-way-merge'라고 부르는데요. 이름이 '3-way'인 이유는 다음과 같습니다.

첫번째로 두 갈래로 갈라지기 전 공통 조상이 되는 커밋과, 두번째로 한 브랜치가 가리키는 커밋, 세번째로 다른 브랜치가 가리키는 커밋. 총 세 가지의 커밋이 존재하기 때문이죠.

'3-way merge'는 자신만의 방식을 가지고 이 세 가지 커밋을 기준으로 merge 커밋을 자동으로 만들어냅니다. 그 방식을 간단히 알려드리자면, 다음 표와 같습니다.

각 컬럼(열)에 대해 설명하겠습니다. 모든 커밋에 sample.txt 파일이 있다고 가정하면,

  1. base: 두 브랜치의 공통 부모 커밋의 sample.txt 파일의 내용 중 일부
  2. master: master 브랜치의 최신 커밋의 sample.txt 파일의 내용 중 일부
  3. premium: premium 브랜치의 sample.txt 파일의 내용 중 일부
  4. merge 결과: master 브랜치에서 premium 브랜치를 merge했을 때의 최종 결과

다음으로 각각의 경우에 왜 위와 같은 merge 결과가 나타나는지를 설명하겠습니다.

❗ CASE1

base가 A이고 master는 A, premium은 B입니다. 그럼 base를 기준으로 볼 때, master에서는 변화가 없었지만 premium에서는 A가 B로 변경된 상태입니다. 3-way merge는 base에서 변화가 발생한 것을 우선 채택합니다. 따라서, merge 결과는 B가 됩니다.

❗ CASE2

base가 1이고 master는 2, premium은 1입니다. 이 경우에도 base에서 변화가 발생한 2가 merge 결과가 됩니다.

❗ CASE3

base가 "hello"이고 master는 "hello"를 삭제한 공백 상태, premium은 "hello"입니다. 마찬가지로 변화가 일어난 공백 상태가 merge 결과가 됩니다.

❗ CASE4

base가 "bye", master가 "fighting", premium이 "please"입니다. 이전 경우들과는 다르게 master와 premium 모두 변화가 있습니다. 이때, Git은 어떤 변화를 선택할까요? 정답은 '모른다!'입니다. 앞서, 같은 함수의 이름을 다르게 해서 브랜치 할 경우 conflict가 발생한다고 배웠었죠? 이와 같은 사례입니다.

이제 3-way merge의 원리가 이해되시나요? 정리하자면, base의 내용과 비교했을 때 달라진 부분이 merge 결과로 선택되고 두 브랜치 모두 변화가 있을 때에는 conflict를 발생시켜 사용자 스스로가 선택하게끔 합니다.

심화 내용인만큼 당장 이해가 되지 않더라도 괜찮습니다. 다만 merge의 종류에는 크게 Fast-foward merge3-way merge가 있다는 사실만큼은 기억해 주시길 바랍니다.

🌿 브랜치 커맨드 정리

git branch [새 브랜치 이름]은 새로운 브랜치를 생성하는 커맨드이고 git checkout -b [새 브랜치 이름]은 새로운 브랜치를 생성할뿐만 아니라 해당 브랜치로 바로 이동시켜주는 커맨드입니다.

git branch -d [새 브랜치 이름]은 브랜치를 삭제하는 커맨드이며, git checkout [기존 브랜치 이름]은 해당 브랜치로 이동하는 커맨드입니다.

git merge [기존 브랜치 이름]은 현재 브랜치에 다른 브랜치를 merge하는 커맨드이고 git merge --abort는 merge를 하던 도중에 conflict가 발생했을 때, 일단 merge 작업을 취소하고 이전 상태로 돌아가게 해주는 커맨드입니다.


이번 시간에는 git reset과 브랜치 사이의 관련성, git reset과 git checkout 동작 원리의 차이, merge 커밋을 생성하지 않는 merge 종류에 대해 배웠습니다. 배운 내용 모두 심화된 내용이기 때문에 이해하기가 쉽지는 않습니다. 우선은 앞서 배운 브랜치의 내용에 집중해주시고 브랜치에 대해 더 깊게 알고 싶을 때 다시 찾아와서 읽어보시면 좋을 것 같습니다.

다음 시간에는 Git의 실무! Git으로 협업하는 방법에 대해 함께 알아보겠습니다.

* 이 자료는 CODEIT의 'Git으로 배우는 버전 관리' 강의를 기반으로 작성되었습니다.
profile
There's Only One Thing To Do: Learn All We Can

0개의 댓글