저번 시간에는 하나의 파일로부터 여러 버전을 나누는 브랜치의 개념과 활용 방법에 대해 함께 배웠습니다.
이번 시간에는 지금까지 배운 Git 지식을 총동원하여, 실무에서 Git을 잘 활용할 수 있는 방법과 팁에 대해 알려 드리려 합니다.
우리는 지금까지 Git과 Git 커맨드, GitHub과 로컬 레포지토리, 리모트 레포지토리, 커밋, 브랜치, merge, reset 등 Git에 관한 여러 개념들을 배워왔습니다.
코드는 slave 브랜치에서 수정하시고 작업 끝나시면 커밋한 뒤에 remote에 push해주세요. OO님이 작업하신 건 master 브랜치에 merge할 겁니다.
이제는 위 요구가 어떤 내용인지 파악이 되시나요? 앞으로는 이때까지 배운 내용을 바탕으로 실제 개발 환경에서 Git을 사용하면서 겪는 상황들과 그 상황에서 사용할 수 있는 커맨드들을 배워보려 합니다.
만약 실무에서 배우지 않았던 커맨드들이 등장하더라도 Git의 기본 원리를 파악하고 있는 여러분들은 어렵지 않게 상황을 대처할 수 있을 것이라 생각합니다.
배우다가 헷갈리는 부분이 생기면 다시 복습해주세요. 이후부터는 Git에 대해 완벽히 이해하고 있다는 가정 하에 진행될 것이기 때문이죠.
git push
는 로컬 레포지토리에 있는 내용을 리모트 레포지토리에 보낼 때 사용하는 커맨드이고 git pull
은 리모트 레포지토리에 있는 내용을 로컬 레포지토리에 보낼 때 사용하는 커맨드입니다.
이와 관련된 간단한 실습을 해보겠습니다. 먼저, master 브랜치에서 premium 브랜치로 이동해주세요.
git checkout premium
다음으로, 라이센스 파일을 잠깐 살펴보겠습니다.
cat License
현재는 premium이라는 문구만 간단히 적혀있네요. 이 상태에서 premium 버전에 관해 좀 더 자세한 정보를 추가하려 합니다. Sublime Text로 이동해서 기업용 premium 버전의 가격에 대한 정보를 추가하겠습니다.
그 다음 파일을 저장해주세요. 수정 후에는 커밋을 해야겠죠? Git bash로 돌아와서 커밋을 진행하겠습니다.
git add .
git commit -m "Update License for enterprise use"
로컬 레포지토리의 premium 브랜치에서 커밋을 했으니 다음은 리모트 레포지토리의 premium 브랜치 차례입니다. GitHub 페이지로 이동해주세요. 그 다음, branches
를 클릭하고 premium
을 클릭해주세요.
여기서 premium 브랜치인지 확인하려면 상단 브랜치 모양에 premium
이 적혀 있는지를 보거나 주소창에 /premium
이 있는지 찾아보면 됩니다.
이제 GitHub의 premium 브랜치에서도 License 파일을 수정해보겠습니다. License 파일에 들어가보면,
아직 git push를 하지 않아 처음 License 파일을 봤을 때처럼 premium이라는 글만 간단히 적혀 있네요. 우측 연필 모양을 클릭하여 내용을 수정해보겠습니다.
여기서는 로컬 레포지토리에서와 다르게 교육용을 위한 가격을 적겠습니다. 아래 커밋 메시지에는 Update License for educational use
라고 적어주세요. 그 다음 녹색 커밋 변경 버튼을 누르면 됩니다.
지금까지의 상황을 정리해보겠습니다. 로컬 레포지토리와 리모트 레포지토리가 가리키는 최신 커밋이 서로 다릅니다. 로컬에서는 기업용 요금 정보를, 리모트에서는 교육용 요금 정보를 추가했으니까요.
리모트 레포지토리에서 했던 작업을 본인이 아닌 동료 개발자가 했다고 가정해봅시다. 그럼, 한 프로그램을 다른 개발자가 다른 방식으로 개발했다고 볼 수 있는데요. 이런 일은 실무에서 아주 흔하게 발생합니다.
다시 한 번 상황을 정리해드리겠습니다. 현재, 동료 개발자가 자기 컴퓨터에서 premium 브랜치에 교육용 요금 정보를 추가하고 커밋했습니다. 그리고 이를 그대로 리모트 레포지토리의 premium 브랜치로 git push를 하게 되면 제 로컬 레포지토리에 있는 premium 브랜치와는 내용이 달라집니다. 리모트 레포지토리를 중심으로 협업을 하다보면 이런 일은 자연스럽게 벌어지는 현상입니다.
지금 이 상태에서 제 로컬 레포지토리에서 git push를 하면 어떻게 될까요? 한 번 해보겠습니다.
git push
에러가 납니다. 그 이유는 이미 다른 개발자가 추가한 내용이 있는데 그것과 다른 내용을 반영해버리면 고생해서 만든 내용이 덮어씌워지기 때문입니다.
이처럼 로컬 레포지토리를 수정하는 동안 이미 리모트 레포지토리에 변화가 생겼다면 바로 git push할 수 없습니다. 그럼 어떻게 해야할까요? 이럴 때는 일단 git pull을 해야합니다. 리모트 레포지토리의 내용을 먼저 가져와 보는 거죠.
git pull
그런데 이번에도 에러가 나네요. 에러 메시지를 보니 낯이 익습니다. 바로 CONFLICT가 발생한 건데요. 어떨 때, CONFLICT가 발생한다고 했었죠? 네, 맞습니다. merge를 할 때, 서로 다른 내용이 등장하면 Git이 어떤 내용을 선택해야 할지 몰라 경고 메시지를 띄운다고 했습니다. 그런데 왜 CONFLICT가 뜬 걸까요?
git pull을 하면 리모트 레포지토리의 premium 브랜치에 있는 최신 커밋들을 로컬 레포지토리의 premium 브랜치로 가져와 그대로 merge 합니다. 즉, git pull은 최신 커밋을 가져와 merge까지 하는 커맨드인 거죠. 따라서, merge를 하려 하니 CONFLICT가 발생했던 겁니다.
이를 해결하는 방법은 앞서 배웠던 CONFLICT를 해결하는 방법과 동일합니다. 파일을 직접 수정해주는 것이죠.
Sublime Text로 돌아와보니 역시나 파일 내용에 이상한 문구들이 추가되었습니다. 이제 위 내용들을 원하는 모습대로 수정해주면 됩니다. 기업용 요금 정보와 교육용 요금 정보가 모두 필요하니 두 정보를 모두 반영해보겠습니다.
이렇게 내용을 수정해주고 저장한 뒤, 터미널에서 git add까지 마쳐주세요. 이번에는 commit 메시지 없이 커밋하겠습니다.
git commit
그럼 에디터 창이 뜨는데요.
노란 문구를 해석해보면 리모트 레포지토리의 premium 브랜치를 로컬 레포지토리의 premium 브랜치로 merge한다는 뜻입니다. 이는 git pull의 연장선상에 있기 때문인데요. git pull을 하다 실패해서 내용을 수정한 뒤 커밋을 진행했기 때문에 위와 같은 문구가 뜬 것이죠. 그리고 이 merge를 잘 마무리하게 되면 성공적으로 git pull이 완료됩니다. :wq
를 통해 메시지를 저장하고 에디터를 나가주세요.
이렇게 새로운 커밋이 추가됨으로써 git pull이 잘 완료되었습니다.
지금 상태를 다시 한 번 보겠습니다.
git history
그럼 최신 커밋으로 방금 진행한 커밋이 뜨는 것을 확인할 수 있습니다. 이 상태에서 git push를 진행해보겠습니다.
git push
이번에는 성공적으로 git push가 완료됩니다. git push가 잘 되었는지 GitHub 페이지에서도 확인해봅시다.
잘 반영되었네요. 혹시 반영이 안 되어 있다면 새로고침을 한 번 눌러보세요.
정리해봅시다. 서로 다른 개발자가 동일한 리모트 레포지토리의 내용을 가져와 각각 새로운 커밋을 했습니다. 이때, 개발자 B가 먼저 git push를 해버리면 개발자 A는 바로 git push를 할 수 없습니다. 이럴 때에는 git pull을 통해 리모트 레포지토리의 해당 브랜치에 있는 다른 개발자의 커밋을 가져와 자신의 로컬 레포지토리에 반영시켜야 합니다. 이는 리모트 레포지토리의 브랜치를 로컬 레포지토리에 merge하는 것과 같습니다.
그런데 이때, git pull이 바로 성공하는 경우도 있지만 대부분은 conflict 에러가 납니다. 그럼 conflict 에러를 해결해주고 다시 커밋을 진행하면 됩니다. 그 다음, git push를 해주면 정상적으로 push가 완료됩니다.
현실에서는 여러 개발자가 하나의 프로젝트에 대해 협업하는 경우가 많은데요. 따라서, 이와 같이 git push가 바로 안 되는 상황이 오히려 일반적이라고 할 수 있죠. 그럴 때는 당황하지 말고 git pull부터 진행해봅시다.
브랜치를 가져온다는 것은 브랜치가 가리키고 있는 커밋 이전에 이루어진 모든 커밋들을 가져온다는 뜻입니다. 그런데 이때, merge는 하지 않고 가져오는 것만 해주는 커맨드도 있는데요. 바로 git fetch
입니다. fetch는 우리말로 가져오다라는 뜻을 가지고 있습니다.
git pull을 하면 merge까지 자동으로 해줘서 편할 것 같은데 왜 굳이 git fetch를 써야 할까요? git fetch는 리모트 레포지토리에 있는 브랜치의 내용을 일단 가져와서 살펴본 후에 merge하고 싶을 때 사용합니다.
더 구체적인 사례를 들어보겠습니다. 회사에 신입 개발자가 들어왔다고 합시다. 그리고 이 개발자가 우리가 만든 calculator.py를 실수로 잘못 수정했다고 가정해 봅시다.
GitHub 페이지에서 premium 브랜치의 calculator.py 파일을 엽니다.
현재는 이와 같은 코드들이 보입니다. 이제 연필을 눌러 내용을 수정하겠습니다.
코드 마지막에 say_hi라는 함수를 추가했습니다. 커밋 메시지에 Add say_hi function
를 적어준 후, 커밋까지 진행합니다. 그런데 say_hi는 계산기 프로그램과는 전혀 무관한 함수이죠? 이 상태로 git pull을 해버리면 어떻게 될까요? 잘못된 코드의 내용이 우리의 로컬 레포지토리에 merge 되어 버립니다.
물론 git pull을 하고 나서 잘못된 코드가 없는지 확인해볼 수도 있지만 조금 번거롭습니다. 그래서 이와 같이 바로 git pull을 하기 조금 꺼려지는 상황에서는 git fetch를 통해 리모트 레포지토리의 브랜치 내용을 가져오기만 하는 게 좋습니다.
git fetch
실행하면, origin에 있던 premium 브랜치의 커밋이 로컬 레포지토리로 들어오게 됩니다(premium -> origin/premium
). 리모트 레포지토리의 입장에서는 그냥 premium 브랜치이지만 로컬 레포지토리에서 리모트에 있는 premium 브랜치는 위와 같이 나타납니다.
다음으로 해야할 일은 로컬 레포지토리의 premium 브랜치와 리모트 레포지토리의 premium 브랜치 사이에 어떤 차이가 있는지 비교하는 것입니다. 이전에 git diff에 대해 배웠었죠? 이 커맨드는 커밋 간 차이뿐만 아니라 브랜치 간 차이도 나타내 줍니다.
git diff premium origin/premium
그럼 리모트 레포지토리의 premium 브랜치에 calculator.py 파일에 say_hi 함수가 추가되어 있는 것을 확인할 수 있습니다. 이렇게 하면, 계산기와는 어울리지 않는 say_hi 함수가 포함되어 있다는 것을 알 수 있겠죠?
이때, 문제를 해결하는 방법 두 가지가 있습니다. 첫번째는 잘못된 코드를 추가한 개발자에게 함수를 지우고 다시 리모트 레포지토리에 올려달라고 하는 방법이고, 두번째는 잘못된 부분을 알아서 해결하고 다시 git push를 하는 방법입니다. 현재는 두번째 방법을 활용해 보겠습니다.
먼저, 리모트 레포지토리의 premium 브랜치를 merge 하겠습니다.
git merge origin/premium
성공적으로 merge가 됩니다. Sublime Text에서 calculator.py 파일을 확인해보겠습니다.
say_hi 함수가 들어왔네요. 이 함수는 잘못되었으므로 지워주겠습니다. 그리고 저장한 후, 커밋까지 진행해줍니다.
git add .
git commit -m "Remove say_hi function in calculator.py"
다음으로 git push까지 해주세요.
git push
그럼 리모트 레포지토리의 calculator.py 파일에도 수정 내용이 반영되었을 텐데요. 확인해 봅시다.
다시 원래대로 잘 돌아왔습니다.
정리하자면, git fetch는 리모트 레포지토리에서 가져온 브랜치의 내용을 merge하기 전에 점검해야 할 때 사용합니다. 혹은 리모트 레포지토리에 있는 브랜치의 내용과 현재 작성하고 있는 코드를 비교해서 잘못된 부분이 없는지 검토해야 할 때도 사용합니다. 자기 자신의 코드가 늘 완벽하지는 않기 때문이죠.
git fetch 후에는 git diff를 통해 차이를 비교해봐야겠죠? 그리고 리모트 레포지토리의 브랜치를 검토할 필요 없이 바로 합치고 싶을 때는 그냥 git pull을 사용하면 되겠죠?
Git을 사용하다보면 하나의 파일이 어떻게 변해 왔는지 파악해야 할 때가 있습니다. calculator.py 파일은 여러 커밋을 거쳐 지금의 코드를 가지게 되었는데요. 이처럼 하나의 파일이 완성되기까지 거쳐온 커밋들을 확인할 수 있는 커맨드가 있습니다. 바로 git blame
이라는 커맨드입니다.
blame은 우리말로 '비난하다, 누구의 탓으로 돌리다'라는 뜻을 가지고 있는데요. 표현이 좀 그렇지만 상황을 잘 생각해보면 그럴 듯합니다. 프로그램에 문제가 발생했을 때, 어떤 파일에 특정 코드를 누가 작성했는지 찾아내기 위한 커맨드이기 때문이죠. 그럼 그 코드를 작성한 사람에게 탓을 돌릴 수 있겠죠? 개발자 입장에서는 무시무시한 얘기네요. 😖😖😖 따라서, 항상 좋은 코드를 쓰기 위해 노력해야겠죠?
git blame calculator.py
이렇게 커맨드 옆에는 확인하고 싶은 파일명을 입력해주면 됩니다.
실행해보니 왼쪽에는 커밋 아이디가 나오고 오른쪽에는 작성한 코드가 나옵니다. 그러니까 오른쪽의 코드는 왼쪽 커밋으로부터 탄생한 코드라는 것이죠. 그럼 이 커밋을 누가 한 건지는 어떻게 알 수 있을까요? 커밋 아이디 옆에 있는 아이디가 바로 작성자입니다. 지금은 tataki26
가 작성한 커밋이라고 볼 수 있겠네요.
이 방법 말고 다른 방법도 있는데요. git show
커맨드 기억하시나요? git show 옆에는 궁금한 커밋의 아이디를 적어주면 됩니다.
git show 8e25
Author가 바로 코드 작성자입니다. 만약 이 코드로 인해 문제가 발생한다면 조금 창피하겠네요. 😚😚😚
이처럼 Git은 코드 작성자를 확인할 수 있는 기능을 제공함으로 써 협업 시에 프로젝트 개발자가 좀 더 책임감 있게 코드를 작성하도록 유도하기도 합니다. 한 번 진행한 커밋은 모두 조사 대상이 되므로 항상 제대로 코드를 작성하고 신중하게 커밋을 해야겠습니다.
이번에는 무료 버전을 수정해보겠습니다. master 브랜치로 이동한 후, Sublime Text에 어떤 수의 제곱을 해주는 square 함수를 추가해보겠습니다.
git checkout master
git add 후, 커밋을 완료합니다.
git add .
git commit -m "Add square function"
다음으로 이 커밋을 리모트 레포지토리의 master 브랜치에도 반영해주겠습니다.
git push
그런데 무료 버전을 작업하는 와중에 새로운 소식이 들려옵니다. square 함수를 추가하지 않기로 결정한 건데요. 이미 square 함수를 추가하고 리모트 레포지토리에 반영까지 했는데 좀 당황스럽습니다. 어찌됐든 square 함수를 지우고 다시 커밋을 하면 되겠죠? 이때, 이 동작을 한번에 해주는 Git 커맨드가 있습니다. 해당 커맨드를 배우기 전에 커밋 히스토리부터 보겠습니다.
git history
현재 상태는 이렇습니다. 이 상황에서 git revert
커맨드를 입력하면 최신 커밋에서 한 작업을 되돌리고 커밋까지 해줍니다. revert는 우리말로 '되돌리다, 복귀하다'라는 뜻을 가지고 있습니다. 커맨드 옆에는 취소하고 싶은 커밋의 아이디를 적어주면 됩니다.
git revert 33a5
실행하면, 커밋 메시지를 입력하는 에디터가 뜨는데요. 현재는 노란색 문구로 자동 셋팅 되어 있네요. 이 커밋 메시지를 그대로 사용하겠습니다. 그럼 revert가 완료됩니다.
커밋이 잘 취소되었는지 확인하기 위해 calculator.py를 출력해보겠습니다.
cat calculator.py
sqaure 함수를 추가하기 전 상태로 다시 돌아왔네요. 커밋 히스토리도 다시 한번 확인해보겠습니다.
git history
그럼 최신 커밋이 하나 추가되었네요. 우리는 square 함수를 직접 지우지 않고 square 함수를 추가한 작업을 되돌리는 커밋을 새로 진행했습니다. 이제 이 커밋을 리모트 레포지토리에 반영해보겠습니다.
git push
그리고 다시 커밋 히스토리를 확인해보면,
위 결과와는 다르게 로컬 레포지토리의 master 브랜치와 리모트 레포지토리의 master 브랜치 모두가 최신 커밋을 가리키고 있다는 걸 확인할 수 있습니다.
여기서 'git revert 대신 그냥 git reset을 하면 되지 않을까?'라고 생각하시는 분들이 있을 겁니다. 만약 로컬 레포지토리에서만 작업 중이었다면 그래도 됩니다. 그러나, 리모트 레포지토리를 두고 작업 중이었다면 git reset을 하면 안됩니다.
git reset을 하게 되면 로컬 레포지토리의 master 브랜치가 직전 커밋을 가리키게 되는데 이 상태에서는 git push를 할 수 없습니다. 왜냐하면 현재 리모트 레포지토리에 더 최신 커밋인 Add square function
이 있기 때문이죠. 그럼 위에서 배운 것과 같이 일단 git pull을 하고 git push를 하라는 안내 문구가 뜹니다.
그러나, git revert를 하게 되면 직전 커밋을 가리키지 않고 새로운 커밋을 하게 됨으로써 리모트 레포지토리보다 더 최신 커밋을 가지게 됩니다. 이는 곧, git push가 가능하다는 뜻이죠. git push 후에는 해당 커밋이 리모트 레포지토리에도 반영됩니다. 이것이 바로 git reset 대신 git revert를 사용하는 이유입니다.
사실, 직전 커밋과 git revert를 통해 만드는 새로운 커밋의 내용은 동일합니다. 둘 다 최신 커밋의 그 전 커밋을 담고 있기 때문인데요. 하지만 리모트 레포지토리에 push하기 위해서는 로컬 레포지토리의 내용이 최신 커밋을 가지고 있어야 하므로 이러한 낭비는 감수해야 합니다. 더 좋은 방법은 git push를 좀 더 신중하게 하는 것이죠.
더 과거의, 그리고 여러 개의 커밋을 대상으로 한꺼번에 revert 할 수 있는데요. 한번 해보겠습니다.
앞서, 우리는 README 파일을 더 예쁘게 꾸며주기 위해 여러 마크다운 언어를 붙인 적이 있었습니다. 그 결과는 다음과 같습니다.
그런데 이 README 파일을 다른 팀에서 더 예쁘게 만든 후 전달해 준다고 합니다. 그래서 현재의 README 파일에서 필요한 글만 남긴 후, 전부 초기화 해달라는 부탁을 받았습니다. 그럼 master 브랜치와 premium 브랜치 모두에서 README 파일을 초기화 해줘야겠죠?
먼저, master 브랜치에서 진행해보겠습니다. 커밋 히스토리를 볼게요.
쉽게 구분하기 위해 아이디를 통해 설명해드리겠습니다. 309f 커밋에서 README 파일을 처음 생성하고 9292 커밋에서 README 파일을 업데이트 한 후, 4ee6 커밋에서 README 파일을 예쁘게 꾸며주었습니다.
우리가 해야 할 일은 처음 README 파일을 생성한 309f 커밋을 제외하고 나머지 두 커밋을 revert 하는 것입니다. 이때, 여러 커밋을 revert 하려면 revert 범위를 지정해줘야 하는데요. 다음과 같이 하면 됩니다.
git revert 309f..4ee6
이때, 주의해야 할 것은 앞에 적은 커밋 아이디는 범위에 포함되지 않는다는 것입니다. 따라서, 범위에 포함되는 첫 커밋은 그 다음 커밋인 9292 커밋입니다.
실행을 하면, 커밋 메시지를 적기 위한 에디터가 뜹니다.
커밋 메시지는 셋팅 된 그대로 사용하겠습니다. 저장을 하면,
이번에는 Update README 커밋에 대한 커밋 메시지 입력 창이 뜹니다. 마찬가지로 셋팅 된 그대로 메시지를 사용하겠습니다. 그럼 revert 커밋이 두 개 생깁니다. 정말 README 파일이 초기화 됐는지 확인해봅시다.
cat README.md
확인해보니, 처음 README 파일을 작성했을 때와 같은 내용이 출력됩니다. 커밋 히스토리도 한 번 볼까요?
git history
최신 커밋을 보면 두 revert 커밋이 새로 생긴 것을 확인할 수 있습니다. 이 둘을 리모트 레포지토리에 push 하겠습니다.
git push
그럼 origin/master
가 로컬 레포지토리의 master 브랜치와 같이 최신 커밋을 가리키게 됩니다.
이번에는 premium 브랜치에도 적용해보겠습니다.
git checkout premium
premium 브랜치도 master 브랜치 때처럼 진행하면 됩니다. revert 범위를 지정하고 커밋 메시지를 각각 입력하는 거죠.
이후, 커밋 히스토리를 확인하면 마찬가지로 두 개의 revert 커밋이 생성된 것을 확인할 수 있습니다.
그 다음, git push를 통해 리모트 레포지토리에 두 커밋을 반영합니다. 그럼 두 브랜치 모두 revert 커밋을 가리키게 됩니다.
이런 식으로 범위를 지정해서 여러 커밋을 revert 할 수 있습니다.
git fetch
는 로컬 레포지토리에서 현재 HEAD가 가리키는 브랜치의 upstream 브랜치로부터 최신 커밋들을 가져오는 커맨드입니다. 가져오기만 한다는 점에서, 가져와서 merge까지 하는 git pull
과는 차이가 있습니다.
git blame
은 특정 파일의 내용 한줄한줄이 어떤 커밋에 의해 생긴 것인지 출력하는 커맨드이고 git revert
는 특정 커밋에서 이루어진 작업을 되돌리는(취소하는) 커밋을 새로 생성하는 커맨드입니다.
이번 시간에는 Git을 활용해서 협업할 때, 발생할 수 있는 문제 상황과 그 대처법에 대해 알아봤습니다. 실습을 하면서 중간중간 에러가 났었는데요. Git은 협업에 강한 도구이지만 신중히 사용하지 않으면 동료 개발자의 소중한 작업물을 망가뜨릴 수도 있다는 점에서 기본 원리와 커맨드를 반드시 숙지하고 주의 깊게 사용해야겠다는 생각이 듭니다.
다음 시간에는 Git을 자유자재로 활용할 수 있는 여러 꿀팁들을 소개하려 합니다. 이제 얼마 남지 않았으니 조금만 더 힘내서 따라와주시길 바랍니다.
* 이 자료는 CODEIT의 'Git으로 배우는 버전 관리' 강의를 기반으로 작성되었습니다.