Notice
Recent Posts
Recent Comments
관리 메뉴

즐겁게, 코드

[번역] : Git fetch와 pull, pull은 이제 그만! 본문

🇺🇸 번역

[번역] : Git fetch와 pull, pull은 이제 그만!

Chamming2 2021. 5. 2. 01:57

🏒 들어가기 전에

이 글은 Mark Longair 님의 GIT: FETCH AND MERGE, DON’T PULL 을 읽고 번역한 글입니다.

※ 매끄러운 번역을 위한 의역이 다소 있으며, 혹시 잘못된 번역이나 어색한 문장 지적은 감사히 받겠습니다. 😄

 

2020 오픈소스 컨트리뷰톤 당시 멘토님께서는 원격 저장소에서 커밋을 불러올 때 git fetch와 rebase를 활용했는데, 저로서는 git pull을 두고 왜 어려운 방식을 사용하는 건지 이해되지 않았던 때가 있었습니다.

 

이번 글을 번역하면서 git fetch와 pull의 차이를 정리해보고자 합니다.

(요약 : git pull은 변경사항을 페칭한 후 곧바로 병합하지만, git fetch는 페칭과 병합 프로세스를 분리할 수 있기 때문입니다.)


제가 주변 사람들에게 자주 전파하는 Git 팁입니다.

git pull 대신 git fetch와 merge를 사용해라!

git pull은 마치 마법처럼 간단히 동작하지만 그러다 보니 다른 브랜치들의 종류에 대해 접할 기회가 없는데요, 따라서 대부분의 경우에는 잘 동작하더라도 그러지 않을 때는 원인을 찾기가 쉽지 않습니다. 아무래도 git pull이라는 직관적인 명령어로도 충분히 잘 동작하기도 하고, 메뉴얼 페이지를 대충 훑어보는 것만으로도 간단히 git pull의 동작을 이해할 수 있어 더 깊숙히 알아볼 필요가 없기 때문입니다.

 

pull의 또다른 문제는 한 명령어로 페칭(fetching)과 병합(merging)을 동시에 수행한다는 점으로, git pull을 실행하면 작업 디렉토리는 사용자에게 저장소로 불러온 변경사항에 대해 확인할 기회조차 주지 않습니다. 물론 안전성 검사를 모두 꺼두지 않는 한 git pull을 수행한다고 해서 대참사가 일어나지는 않겠지만, 불러온 변경사항들을 다시 확인할 필요가 없도록 작업(페칭과 병합)을 조금은 천천히 수행하고 싶을 것입니다.

브랜치들

git pull에 대해 설명하기 전에 브랜치가 무엇인지 명확히 해두려 합니다.

브랜치는 "개발의 선(line of development)" 이라고도 표현되는데요, 하지만 저는 아래 이유로 이 표현이 다소 아쉽게 느껴집니다.

  • 거의 언제나 브랜치는 선보다는 "유향 그래프" 에 가깝습니다.
  • 선이라는 이름은 브랜치가 무겁게 느껴지도록 합니다.

저는 브랜치를 있는 그대로 생각하기를 제안합니다.

브랜치는 특정 커밋(commit)과 이전에 존재하는 커밋들의 일람으로, 각 브랜치는 커밋이 갖는 SHA1sum 값을 통해 완벽히 정의될 수 있습니다. 이는 브랜치를 조작하는 것이 굉장히 가벼운 작업이라는 의미로, 여러분은 (커밋의 SHA1sum) 값을 바꾸는게 전부입니다.

다만 이런 정의는 예상 밖의 잘못된 의미를 가질 수도 있습니다.

예를 들어, "stable" 과 "new-idea" 라는 두 브랜치가 있고, 각 끝에는 수정사항 E와 F가 있다고 가정하겠습니다.

  A-----C----E ("stable")
   \
    B-----D-----F ("new-idea")

A, C, E 커밋은 "stable" 브랜치에 있는 상태이며 A, B, D, F 커밋은 "new-idea" 브랜치 상에 있습니다.

다음 명령어로 "new-idea" 브랜치를 "stable" 브랜치로 병합해 보겠습니다.

git checkout stable   # "stable" 브랜치의 작업으로 전환합니다.
git merge new-idea    # "new-idea" 브랜치의 작업을 병합합니다.

그럼 다음과 같은 결과를 얻게 됩니다.

  A-----C----E----G ("stable")
   \             /
    B-----D-----F ("new-idea")

여기서 "new idea" 브랜치와 "stable" 브랜치에 계속 커밋을 남기면, 이런 결과를 얻게 됩니다.

  A-----C----E----G---H ("stable")
   \             /
    B-----D-----F----I ("new-idea")

이제 A, B, C, D, E, F, G, H 커밋은 "stable" 브랜치에 존재하고, "new-idea" 브랜치에는 A, B, D, F, I 커밋이 존재합니다.

브랜치는 특별한 성질을 갖고 있는데요, 가장 중요한 특징은 브랜치에서 작업을 하는 도중에 새로운 커밋을 생성하면 브랜치의 끝은 그 새로운 커밋으로 변경된다는 점입니다. (다행히도 이게 우리가 원하던 동작입니다.)

 

git merge를 통해 병합을 수행하면 사용자는 현재 브랜치에 병합할 브랜치를 지정할 수 있으며, 현재 브랜치에서 작업을 계속 진행할 수 있습니다.

 

브랜치의 이런 성질이 도움이 되는 또다른 상황이 있습니다.

프로젝트의 메인 브랜치 (Ex. "master" 브랜치)에서 작업을 한다고 가정할 때, 여러분이 작업한 내용이 썩 좋지 않다는 것을 뒤늦게 깨달아 이게 topic 브랜치에 있었으면 합니다.

만약 커밋 그래프가 다음과 같다면:

다른 저장소의 최신 작업내용(버전)
      |
      v
  M---N-----O----P---Q ("master")

여러분은 다음 명령어로 작업내용을 별도로 분리하려 합니다. (다이어그램을 통해 브랜치의 상태 변화를 나타냅니다.)

  git branch dubious-experiment

  M---N-----O----P---Q ("master" and "dubious-experiment")

  git checkout master

  # 다음 명령어를 사용할 때 조심해야 합니다: "git status" 명령어를 사용했다고 치고,
  # 작업자는 분명히 "master" 브랜치에 있는 상황이며 "dubious-experiment" 브랜치에는
  # 처음에 작업중인 내용(잘못 작업한 내용)이 존재합니다.

  git reset --hard <SHA1sum of commit N>

       ("master")
  M---N-------------O----P---Q ("dubious-experiment")

  git pull # 또는 무엇이든 "master" 를 업데이트할 수 있는 명령어

  M--N----R---S ("master")
      \
       O---P---Q ("dubious-experiment")

벌써 뭔가 많은 내용을 소개한 것 같네요 :)

브랜치의 종류

불행히도 브랜치를 전문적으로 다루면 어려워지기 시작하는데요, git 이야기에서 브랜치 이야기가 된 것 같네요.

브랜치에는 두 가지 종류가 있습니다.

 

(a) "로컬 브랜치" : git branch를 입력했을 때 보게 되는 브랜치들 (아래에 간단한 예시를 나타냈습니다)

     $ git branch
       debian
       server
     * master

(b) "원격(리모트) 트래킹 브랜치" : git branch -r 을 입력했을 때 보게 되는 브랜치들

(※ 역주 - 원격 트래킹 브랜치는 원격 저장소에서 변경사항을 페칭했을 때, 페칭 시점의 커밋 기록이 담긴 브랜치입니다.)

     $ git branch -r
     cognac/master
     fruitfly/server
     origin/albert
     origin/ant
     origin/contrib
     origin/cross-compile

트래킹 브랜치의 이름은 원격("remote") 저장소의 이름(Ex. 위의 origin, fruitfly, cognac)에 "/" 를 붙인 후, 각각의 원격 저장소의 이름을 조합해 지어집니다.

(원격 저장소의 이름은 다른 저장소를 가리키는 별명에 불과합니다. "git remote" 를 통해 원격 저장소를 추가로 설정할 수도 있지만, "git clone" 을 사용하면 기본적으로 "origin" 이라는 별명을 부여합니다.)

만약 브랜치가 로컬 환경에 어떻게 저장되는지 궁금하다면, 다음 경로를 확인해볼 수 있습니다.

  • .git/refs/heads/ [로컬 브랜치가 저장되는 곳]
  • .git/refs/remotes/ [트래킹 브랜치가 저장되는 곳]

두 브랜치의 타입은 어떤 면에서는 아주 유사한데요, 둘 다 로컬 영역에 커밋을 표현하는 SHA1 sum으로 저장됩니다.

("로컬 영역"이란 말을 강조한 이유는, "origin/master" 라는 원격 트래킹 브랜치를 보고 이 브랜치는 (원격 영역에 존재해) 접근할 수 없는 브랜치라고 생각할 수도 있기 때문입니다.)

 

이런 유사점에도 불구하고 하나의 특징적인 중요한 차이가 있습니다.

  • 사용자는 원격 트래킹 브랜치에서 직접 작업할 수 없기 때문에 원격 트래킹 브랜치를 안전하게 변경하기 위한 방법은 git fetch나 git push의 부수작용을 이용하는 것 뿐인데요, 반면에 로컬 브랜치로 스위칭해 새로운 커밋을 만드는 것은 언제든지 가능합니다.

따라서 원격 트래킹 브랜치를 사용할 때는 다음 작업 중 하나를 수반해야 합니다.

  • git fetch를 통해 업데이트합니다.
  • 현재 브랜치에서 원격 트래킹 브랜치의 내용을 병합합니다.
  • 원격 트래킹 브랜치를 기반으로 새로운 로컬 브랜치를 생성합니다.

원격 트래킹 브랜치를 기반으로 새로운 로컬 브랜치 생성하기

만약 원격 트래킹 브랜치를 기반으로 새로운 로컬 브랜치를 생성하고 싶다면, 새로운 로컬 브랜치를 생성하고 이동할 때처럼 git branch -track 또는 git checkout -track -b 커맨드를 활용할 수 있습니다. 예를 들어, git branch -r를 입력해 origin/refactored 라는 원격 트래킹 브랜치를 찾았다면 다음 명령어를 사용할 수 있습니다.

git checkout --track -b refactored origin/refactored

위 예시에서 "refactored" 는 새로 생성한 브랜치의 이름이며, "origin/refactored"는 기반이 된 원격 트래킹 브랜치의 이름입니다.

(+ 최근 git 버전부터는 --track 옵션을 제외한 채로 입력해도 동일하게 동작합니다.) 

"--track" 옵션은 원격 트래킹 브랜치가 로컬 브랜치와 이어지도록 하는 설정 변수를 준비하는데요, 이는 크게 두 가지 점에서 유용합니다.

  • git pull을 통해 새로운 원격 트래킹 브랜치를 페칭했을 때, 어떤 커밋 뒤에 병합해야 할지 알 수 있습니다.
  • git checkout을 통해 로컬 브랜치로 이동했을 때, 다음과 같은 유용한 메시지를 알려줍니다.
# 메시지 내용은 git 시스템이 출력하는 것이므로 별도로 번역하지 않았습니다.

Your branch and the tracked remote branch 'origin/master'
have diverged, and respectively have 3 and 384 different
commit(s) each.

또는...

Your branch is behind the tracked remote branch
'origin/master' by 3 commits, and can be fast-forwarded.

이런 기능들을 제공하는 설정 변수는 "branch.<로컬-브랜치-이름>.merge" 와 "branch.<로컬-브랜치-이름>.remote" 라는 이름으로 호출되는데요, 사용자는 이를 어떻게 호출해야 하는지 신경쓸 필요가 없습니다.

 

사용자는 생성된 원격 저장소를 클론한 후 git branch -r 을 입력하면 많은 원격 트래킹 브랜치 목록이 나타나는 것을 확인할 수 있을 텐데, 여러분은 하나의 로컬 브랜치만을 갖는 상태입니다. 바로 이런 경우, 로컬 브랜치가 원격 트래킹 브랜치를 트래킹하도록 할 때 위의 명령어 (※ git checkout --track -b refactored origin/refactored)가 필요한 것입니다.

 

이제 원격 저장소로부터 커밋을 업데이트하는 예시와 함께 새로운 저장소에 변경사항을 푸시하는 방법을 알아보도록 하겠습니다.

원격 저장소로부터 업데이트하기

"origin" 이라는 별명의 원격 저장소로부터 변경사항을 불러오고 싶을 때, git fetch를 입력하면 다음과 같은 결과창이 나타납니다.

  remote: Counting objects: 382, done.
  remote: Compressing objects: 100% (203/203), done.
  remote: Total 278 (delta 177), reused 103 (delta 59)
  Receiving objects: 100% (278/278), 4.89 MiB | 539 KiB/s, done.
  Resolving deltas: 100% (177/177), completed with 40 local objects.
  From ssh://longair@pacific.mpi-cbg.de/srv/git/fiji
     3036acc..9eb5e40  debian-release-20081030 -> origin/debian-release-20081030
   * [new branch]      debian-release-20081112 -> origin/debian-release-20081112
   * [new branch]      debian-release-20081112.1 -> origin/debian-release-20081112.1
     3d619e7..6260626  master     -> origin/master

실행 결과 중 가장 중요한 부분은 바로 이곳입니다.

   3036acc..9eb5e40  debian-release-20081030 -> origin/debian-release-20081030
   * [new branch]      debian-release-20081112 -> origin/debian-release-20081112

첫 번째 줄은 origin/debian-release-20081030이라는 원격 트래킹 브랜치가 3036acc 커밋에서 9eb5e40 커밋까지 확장되었다는 의미입니다. (화살표 전의 내용은 원격 저장소의 브랜치 이름을 의미합니다.)

두 번째 줄은 새로운 원격 트래킹 브랜치가 만들어졌음을 보여줍니다. (git fetch는 원격 브랜치에 있는 새로운 태그들도 페치해옵니다.)

 

이 두 줄 이전의 내용들은 로컬 저장소로 git fetch가 불러와야 할 내용들을 표시하는데요, 따라서 업데이트한 브랜치와 태그들을 사용하는 것이 가능합니다.

 

git fetch는 작업 트리에 전혀 간섭하지 않아, 사용자에게 변경사항을 내려받은 이후 수행할 작업을 선택할 수 있는 여지를 줍니다.

(역주 - git pull은 새로운 변경사항을 내려받고 곧바로 적용하지만, git fetch는 변경사항을 내려받기만 할 뿐 곧바로 적용하지는 않습니다.)

원격 브랜치에서 불러온 내용을 작업 트리에 실제로 적용하려면 git merge를 수행하면 됩니다.

 

예를 들어 사용자가 master 브랜치에서 작업하고 있다고 한다면 다음 명령어를 통해 origin에서 불러온 내용들을 병합할 수 있습니다.

  git merge origin/master

만약 곧바로 변경사항을 병합하는 대신, 여러분의 브랜치와 원격 브랜치의 차이점만을 확인하고 싶다면 아래 명령어를 사용할 수 있습니다.

  git diff master origin/master

이는 페칭과 병합 과정을 분리해서 얻을 수 있는 장점입니다. 이렇게 함으로써 사용자는 변경사항을 페칭한 이후 수행할 작업을 정할 수도 있고, 브랜치의 이름을 통해 로컬 브랜치와 원격 트래킹 브랜치를 사용할 때를 명확히 구분할 수도 있습니다.

원격 저장소에 변경사항 푸시하기

이번에는 다른 방법을 시도해 보겠습니다. "experimental" 이라는 브랜치에 변경사항이 생겼다고 가정하고, 이를 "origin" 이라는 별명의 원격 저장소로 푸시하고자 합니다. 이는 다음 명령어로 간단하게 수행할 수 있습니다. 

  git push origin experimental

아마 원격 저장소가 브랜치를 빨리감기(fast-forward) 할 수 없다는 에러를 볼 수도 있는데, 이런 에러가 나타난다면 누군가가 이미 다른 변경사항을 해당 브랜치에 푸시했을 가능성이 있습니다. 따라서, 이런 경우에는 변경사항을 다시 푸시하기 전에 원격 저장소를 페치한 후 변경사항을 병합할 필요가 있습니다.

(※ 역주 - 빨리감기 병합에 대해서는 링크의 그림 21번을 참고하실 수 있습니다.)

작은 팁

만약 브랜치와 원격 저장소의 브랜치의 이름(예를 들어, “experiment-by-bob”)이 다르다면, 이런 명령어를 사용할 수 있습니다.

  git push origin experimental:experiment-by-bob

결론 : 왜 git pull을 사용해서는 안되는가?

git pull은 거의 언제나 잘 동작하며, git을 편의점처럼 가볍게 사용하고 싶을 때 적합한 명령어입니다.

그러나 수많은 브랜치를 생성하고 로컬 커밋 히스토리를 변경하는 등, git을 보다 있는 그대로 사용하고 싶다면 git pull보다는 git fetch와 merge 과정을 분리하는 것이 훨씬 더 유용할 것입니다.

반응형
Comments
소소한 팁 : 광고를 눌러주시면, 제가 뮤지컬을 마음껏 보러다닐 수 있어요!
와!! 바로 눌러야겠네요! 😆