1장. 도입부

Github에 대해 설명하기 쉽게 비유법을 사용하여 버전 관리 시스템 (Version Control System; 이하 VCS)를 설명해보려 합니다. 제가 하려는 설명에 비해 덜 정신나간 버전의 설명을 원하시면 http://en.wikipedia.org/wiki/Revision_control 를 방문하시길 권장합니다.

"일하는 것"은 곧 "노는 것"

저는 거의 평생을 컴퓨터게임을 하며 지냈습니다. 그에 반해, 어른이 되서야 VCS를 사용하기 시작했지요. 이런 건 저 혼자가 아닐 것이라 생각하니, Git을 게임에 비유하며 설명하는 것이 Git을 이해하는 데 도움이 될 것이라 생각합니다.

자, 이제 코드나 문서를 편집하는 작업이 게임을 하는 것과 같다고 생각해보세요. 편집을 마친 후에는 세이브하고 싶겠지요? 그렇게 하기 위해서는 당신의 든든한 코딩에디터에서 세이브 버튼을 누르면 될 것입니다.

그러나 단순히 '세이브’를 누르는 것은 예전 세이브를 덮어쓰는 결과를 초래하죠. 세이브 슬롯이 한 개밖에 없는 옛날 구형 게임을 생각하면 됩니다. 다시 말하자면, 세이브를 할 수는 있지만, 예전 세이브포인트로 돌아갈 수 없는 것 입니다. 마치, 게임 진행하다가 정말 재미있는 파트에 적재적소에 맞게 세이브를 해 놓았는데 그 포인트로 다신 돌아갈 수 없다는 것이죠. 좀 더 심하게 말하자면, 절대로 깰 수 없는 보스 앞에서 세이브를 한 당신은 그 상태에 평생 머무르게 될수도 있다는 것 입니다. 그럴 경우에는 아주 처음부터 다시 시작해야 된다는 말이 되겠지요.

버전 관리

코드 등을 편집 시, 다른 이름으로 저장 아니면 사본을 다른 디렉토리에 카피 떠 놓는 방법 등을 이용해 옛 버전의 코드들을 보존할 수는 있습니다. 컴퓨터 용량을 더욱 효율적으로 사용하기 위해서 압축을 할 수도 있죠. 이것은 참 원시적인 버전 컨트롤 방법입니다. 컴퓨터게임은 이런 과정에서 이미 발전해 나간지 오래되었지요. 요즘 게임들은 여러개의 세이브 슬롯에 시간을 기록해가며 세이브를 영리하게 해냅니다.

이 문제를 좀 더 꼬아서 바라봅시다. 당신이 어떤 프로젝트나 웹사이트를 구성하는 소스코드와 같이 여러개의 파일을 보유한다고 가정합시다. 현 버전의 프로젝트/웹사이트를 세이브하고 싶다면 모든 디렉토리를 기록해야 한다는 번거로움이 있겠지요. 일일이 그 수 많은 버전들을 수동으로 관리한다는 것은 그리 효율적이지 않을 겁니다. 컴퓨터 용량도 더불어 많이 사용하게 될꺼고요.

어떤 컴퓨터게임들은 정말로 모든 디렉토리를 각개 관리하는 형식으로 게임을 세이브하기도 합니다. 이런 게임들은 이런 불필요하게 세부적인 사항들을 게이머들이 보지 못 하게 하고 간편한 인터페이스를 통해 게이머들이 세이브파일들을 관리할 수 있게 해둡니다.

VCS는 이런 컨셉과 그리 다르지 않습니다. VCS들은 파일 디렉토리들을 관리하기에 아주 편한 인터페이스로 구성되어 있습니다. 원하는 횟수 만큼 세이브를 할 수 있고, 원하는 세이브포인트를 특정지어 불러오기를 실행할 수도 있습니다. 그리고 컴퓨터게임들과는 다르게 용량을 효율적으로 사용하는 데에는 탁월한 성능을 보여주죠. 대부분의 어떤 코드의 버전을 바꿀 때에는 소수의 파일들만 살짝 바꾸게되죠. 코드자체가 아주 많이 바뀌는 경우는 드뭅니다. 디렉토리 전체를 세이브하는 것 보다는 버전과 버전사이의 차이를 세이브하는 것이 용량을 효율적으로 쓰는 VCS의 비밀입니다.

분산 제어

여러분이 어려운 컴퓨터 게임을 한다고 생각해보세요. 너무 어렵기 때문에 전 세계의 프로게이머들이 팀을 구성해 이 게임을 끝내보겠다고 합니다. 게임을 빨리 끝내는 것에 초점을 두는 스피드런 방식의 게임 스타일이 현실적인 예시 이지요: 각기 다른 특기를 가지고 있는 게이머들이 한 게임 안에서 각자 자신있는 부분을 담당함으로써 성공적인 결과를 만들어내는 것을 예로 들어봅니다.

어떻게 시스템을 구축해 두어야 게이머들이 서로의 세이브파일들을 쉽게 업로드 하거나, 바통을 이어 받을 수 있을까요?

프로그래밍 프로젝트들은 예전에 중앙 집중식 VCS를 사용하였습니다. 한 개의 서버가 모든 세이브파일을 저장했었지요. 그 서버외에는 아무 것도 그 세이브파일들을 관리할 수 없었습니다. 게임으로 말하자면, 게이머들은 각자의 게임기에 몇 개의 세이브파일들을 가지고 있었고, 게임을 다른 사람으로부터 이어받아 진행하고 싶을 때에는, 모든 세이브파일들이 저장되어있는 중앙서버에서 파일들을 다운로드 받은 후, 게임을 좀 하다가, 다시 다른 게이머들이 진행할 수 있게 그 서버에 업로드 해 놓아야 합니다.

만약에 어떤 한 게이머가 예전에 세이브 해두었던 오래된 파일을 불러오고 싶다면 어떻게 될까요? 현재 최신의 세이브 시점은 누군가 게임의 전 단계에서 다음 단계에 필요한 아이템을 주워오지 않아서 아무리 잘 해도 게임을 진행 할수없는 상태로 저장이 되어있을지도 모르고, 그런게 아니라면 그들은 아마도 세이브파일 두 개를 비교하여 한 특정 게이머가 얼마나 진행을 하였는지 알고 싶어할지도 모릅니다.

예전 세이브 파일을 불러오고 싶은 이유는 여러가지일 수 있습니다, 그러나 방법은 한 가지일 수 밖에 없지요. 중앙서버에서 불러오는 방법 말입니다. 더 많은 세이브파일을 원할 수록 서버와의 통신이 더 잦아질 수 밖에 없지요.

(Git을 포함하여) 새로운 세대의 VCS들은 분산 제어를 기본으로 합니다. 예전의 중앙관리 방식의 보편화된 방식이라고 생각하면 되지요. 한 게이머가 서버로부터 (가장 최신) 세이브파일을 받는다해도 그 하나만 받게되는 것이 아니라 모든 예전 버전의 세이브파일까지도 같이 받게 되는 겁니다. 마치 중앙서버를 각자의 컴퓨터에 미러링한다고 보시면 됩니다.

그렇기에 처음에는 Git을 셋업할 때 시간이 많이 걸릴 수 있습니다. 특히, 그 세이브파일이 오래되었고, 아주 긴 역사를 가지고 있다면 말이지요. 그러나 이 것은 길게보면 아주 효율적인 방법입니다. 이 방법을 통해 즉시 이득을 볼 수있는 점을 따진다면, 예전 세이브파일을 원할 때 중앙서버와 교신을 하지 않아도 된다는 점이지요.

멍청한 미신

사람들이 분산 제어 시스템에 대해 일반적으로 오해하는게, 분산 제어 시스템은 공식적인 중앙 저장소가 필요한 프로젝트에는 적합하지 않다고 생각하는 것입니다. 이 것은 말도 안되는 오해이지요. 이 오해는 누군가의 사진을 찍는다는 것은 그 피사체의 영혼을 같이 담아버린다는 말도 안 되는 논리와 같습니다. 다시 말하면, 중앙 저장소의 파일을 카피하여 분산 제어하는 것이 중앙 저장소의 중요성을 훼손한다는 것이 아닙니다.

첫번째 이해해야하는 부분이, 중앙 버전 관리 시스템이 할 수 있는 모든 일들은 잘 짜여진 분산 관리 시스템이 더 잘 할수 있다는 것을 인식해야 한다는 것입니다. 네트워크상의 자원들은 기본적으로 로컬상의 자원들보다 시간적으로나 물질적으로 비경제적일 수 밖에 없습니다. 물론, 나중에도 말씀드리겠지만 분산 제어 시스템도 문제점이 없는 시스템은 아닙니다. 그러나 주먹구구식의 생각으로 중앙 관리 시스템과 분산 관리 시스템을 비교하는 일은 없어야 할 것입니다. 다음 인용문이 이것을 대변해 줍니다.

"규모가 작은 프로젝트들은 시스템의 부분적인 특성만으로도 실행 할 수 있겠지만, 이런 프로젝트들을 스케일업 할 수 없는 확장성이 낮은 시스템으로 계속 실행하는 것은 마치 옛 로마숫자를 이용해 계산기를 두드리는 것과 같다."

더욱이 당신의 프로젝트는 당신이 처음 생각했던 것보다 더 거대한 일이 될지도 모르는 겁니다. 처음부터 Git을 사용한다는 것은 단순히 병뚜껑을 여는데 스위스아미나이프를 들고 다니는 것과 같은 것입니다. 그러나, 어느 날, 드라이버가 필요할 경우, 당신은 병따개만 들고다니지 않았다는 사실에 안도의 한 숨을 쉬게 될 것입니다.

병합 충돌

이 주제를 설명하기 위해서는 컴퓨터게임에 비유하는 것은 더 이상 적합하지 않을 수 있습니다. 대신에 여기서는 문서편집에 비유해서 설명드리도록 하죠.

다음 예시를 생각해 봅시다. 앨리스(Alice)는 파일을 편집하던 도중에 새로운 줄을 첫 줄로 추가하고, 밥(Bob)은 그 같은 파일의 마지막에 코드 한 줄을 더한다고 가정합시다. 그리고 그들은 편집된 파일을 각자 중앙서버에 업로드 합니다. 대부분의 시스템은 자동으로 두 사람이 각자 한 편집을 받아들이고 병합할 것입니다. 결과적으로는 앨리스와 밥 두 사람의 편집이 모두 한 파일에 적용되겠죠.

자 이제 앨리스와 밥이 어떤 파일의 정확히 같은 부분에서 서로 다른 편집을 한다고 가정해 봅시다. 이럴 경우에는 인간의 직접적인 개입없이는 온순한 편집이 불가능 하겠지요? 누가 편집을하던 두번째로 편집하는 사람은 작업을 결합하는 과정에서 오류메세지 ("merge conflict")를 볼 수밖에 없겠지요. 이런 오류메세지를 피하기 위해선 한 사람만의 작업을 선택하거나 두 사람의 작업을 아우를 수 있는 새로운 코딩작업을 추가로 해줘야하는 번거로움이 발생하지요.

예시보다 더 복잡한 상황이 일어날수도 있습니다. VCS는 간단한 상황들을 알아서 해결해 주고, 어려운 상황은 인간의 손에 맡기지요. 이런 VCS의 행동은 대체적으로 조정가능합니다.

==기본적인 요령==

Git의 수많은 명령어 속으로 곧바로 다이빙 하는 것 보단, 다음 간단한 예시들을 통해서 천천히 배우는 방법이 좋을 것 같습니다. 제가 소개하는 예시들은, 표면적으로는 간단하게 보이지만, 앞으로 여러방면으로 많은 도움이 될 것입니다. 저 역시도 처음 Git을 사용할 때에는 아래에 있는 예시 외에는 건들여 보지도 않았습니다.

상태 (state) 저장하는 방법===

파일에 무엇인가 큰 변화를 주고 싶으시다고요? 그러시기 전에, 현 디렉토리에 들어있는 모든 파일의 스냅샷을 찍어봅시다:

$ git init
$ git add .
$ git commit -m "My first backup"

위 명령어들을 입력 후, 만약에 편집을 하다가 잘못됬다면, 편집되기 전의 깨끗한 버전으로 되돌리면 됩니다:

$ git reset --hard

또 어떤 작업 후 state를 저장하고 싶다면:

$ git commit -a -m "Another backup"

파일 더하기 (add), 지우기 (delete), 이름 바꾸기 (rename)

위의 간단한 요령들은 처음 git add 명령어를 실행했을 때 이미 존재하던 파일들만 저장하게 됩니다. 존재하던 파일의 편집 이외의 새로운 파일들이나 하위 디렉토리들을 추가했다면, Git에게 알려줘야 합니다:

$ git add readme.txt Documentation

그리고 만약에 원하지 않는 파일을 Git에서 없애려면 그것 역시 Git에게 알려줘야 합니다:

$ git rm kludge.h obsolete.c
$ git rm -r incriminating/evidence/

이렇게 함으로써 Git은 지정한 파일들을 지워주게 됩니다.

Git 파일이름을 바꿀때에는 원치않는 일반파일들의 이름을 지우고 새로운 이름을 새롭게 지정하는 간단한 절차와 같습니다. 좀 더 손쉬운 방법으로는 git mv 명령어가 있습니다. 예를 들어:

$ git mv bug.c feature.c

고급 undo와 redo

가끔씩은 작업을 하다가 하던 일을 멈추고 전 버전으로 돌아가고 싶다거나, 어느 시점 이후의 모든 편집을 지우고 싶을 때가 있을 것입니다. 그렇다면:

$ git log

이 명령어는 최근의 commit들을 정리한 리스트와 그의 SHA1 hashes를 보여줍니다:

commit 766f9881690d240ba334153047649b8b8f11c664
Author: Bob <bob@example.com>
Date:   Tue Mar 14 01:59:26 2000 -0800

    Replace printf() with write().

commit 82f5ea346a2e651544956a8653c0f58dc151275c
Author: Alice <alice@example.com>
Date:   Thu Jan 1 00:00:00 1970 +0000

    Initial commit.

Hash 앞의 알파벳 몇 개만으로도 commit을 세분화 설정하실 수 있습니다; 다른 방법으로는, 아래의 명령어와 같이 hash 전문을 복사/붙여넣기 하는 방법도 있지요:

$ git reset --hard 766f

위 명령어를 입력하시면 설정된 commit으로 돌아갈 수 있으며 그 후의 새로운 commit들은 영구적으로 삭제됩니다.

가끔씩은 또 아주 예전의 state로 잠시만 돌아가길 원하실 수 있습니다. 그럴 경우에는:

$ git checkout 82f5

이 명령어는 82f5 이후의 commit들을 보존함과 동시에 과거의 시간으로 잠시 돌아가게 해줍니다. 그러나, SF영화에서 처럼, 과거에 돌아간 상태에서 편집을하고 commit을 한다면 또 다른 시간대의 현실을 만들어가게 되는 것이죠. 왜냐하면 당신의 편집이 과거의 편집과는 다르게 입력이 되었기 때문입니다.

이렇게 새롭게 만들어진 대체현실을 'branch (나뭇가지)'라고 부릅니다 에 관해선 추후에 자세히 설명합니다. 지금 알고계셔야 할 것은

$ git checkout master

이 명령어는 과거에서 현재의 state로 돌아오게 해줄 것입니다. 그리고 Git이 유저에게 푸념을 놓기전에 과거에서 편집했던 사항들이 있다면 master branch로 돌아오기전 commit을 하거나 reset을 한번 실행하시길 바랍니다.

게임과 또 다시 비교해본다 하면:

  • git reset --hard: 예전에 세이브 해뒀던 게임으로 돌아가며, 돌아간 시점 이후의 세이브들을 모두 삭제합니다.
  • git checkout: 예전에 세이브 해뒀던 게임으로 돌아가며, 돌아간 시점 이후의 게임들은 처음 세이브와 다른 길을 가게 됩니다. 추후의 모든 세이브들은 다른 branch로써 새로운 현실세계를 만들게 됩니다 에 관해선 추후에 자세히 설명합니다.

예전의 파일/하위 디렉토리들을 되돌리고 싶을 때 다음 명령어를 이용함으로써 필요한 파일/하위 디렉토리만을 되돌릴 수 있습니다:

$ git checkout 82f5 some.file another.file

그러나 이 checkout 명령어가 다른 파일들을 조용히 덮어씌우기 할 수 있다는 점을 알아두세요! 이러한 사고를 방지하고 싶다면 checkout 명령어를 쓰기전에 commit을 이용하세요. Git을 처음 이용하는 분들은 특히 더 조심하시기 바랍니다. 대체적으로 파일이 삭제될까 두려우시다면 *git commit -a*를 우선해놓고 생각하세요.

긴 hash 전체를 복붙하기 싫으시다고요? 그렇다면:

$ git checkout :/"My first b"

이 명령어를 사용함으로써 이 commit message를 사용해서 commit했었던 state로 돌아갈 수 있습니다. 그리고 이 다음 명령어로 5번 스텝 전의 state로 돌아갈 수도 있습니다:

$ git checkout master~5

되돌리기 (Reverting)

법정에서는 어떠한 일에 관해서는 기록에서 지울 수 있습니다. 이런 식으로, Git에서는 원하는 commit을 정해서 없던 일로 할 수 있습니다.

$ git commit -a
$ git revert 1b6d

이렇게 하는 것으로 특정 hash에 대한 commit을 undo 할 수 있습니다. 이렇게 되돌린 state는 새로운 commit으로 인식되어 *git log*에 기록됩니다.

변경기록 만들기

어떤 프로젝트들은 changelog. 필요로 합니다. 다음 명령어를 이용해 변경기록을 만들어 봅시다.:

$ git log > ChangeLog

파일 다운로드하기

Git으로 관리되는 프로젝트 사본을 얻기위해서는:

$ git clone git://server/path/to/files

예를 들어, 본 웹사이트를 만들기 위해 사용한 파일들을 얻기위해서는:

$ git clone git://git.or.cz/gitmagic.git

clone 명령어에 관해 많은 것을 소개하도록 하겠습니다.

최첨단 기술

git clone 명령어를 이용해 어떤 프로젝트의 사본을 다운로드 해뒀다면, 다음 명령어를 이용해 그 프로젝트의 최신버전으로 업데이트 할 수 있습니다:

$ git pull

즉석 발행

당신이 다른 사람들과 공유하고 싶은 스크립트를 작성했다고 가정합니다. 당신은 그들에게 당신의 컴퓨터에서 다운로드를 받으라고 할 수있지만, 당신 친구들이 만약 당신이 해당 스크립트를 편집하는 도중에 받게된다면, 그들은 예상치 못한 트러블에 걸릴 수 있습니다. 이러한 이유 때문에 릴리스 사이클이란 것이 존재하는 것입니다. 개발자들은 개발 중인 프로젝트 디렉토리에 자주 들락날락 거릴 것이고, 그들은 그들이 한 작업이 다른 사람들 앞에 내놓을 만한 상태로 만들어지기 전까지 남들에게 보여주지 않을겁니다.

Git으로 릴리스 사이클을 맞추려면, 당신의 스크립트가 들어있는 디렉토리에서:

$ git init
$ git add .
$ git commit -m "First release"

그리고 당신들 친구들에게 다음 명령어를 사용하도록 하십시오:

$ git clone your.computer:/path/to/script

그들이 이렇게하면 당신의 스크립트를 다운로드 할 수 있을 것입니다. 이 작업은 다른 유저들이 ssh 접근을 할수있다고 가정합니다. 그렇지 않다면, 소유주인 당신이 git daemon 명령어를 쓴 후 친구들에게 다음 명령어를 쓰라고 하십시오:

$ git clone git://your.computer/path/to/script

이렇게 하고 난 다음부터 당신의 스크립트가 준비되었을 때마다 다음 명령어를 실행하면 됩니다:

$ git commit -a -m "Next release"

당신의 친구들은 다음 명령어를 사용함으로써 가장 최근 버전으로 당신의 스크립트를 보유하고 있을 수 있게 되죠:

$ git pull

그들은 절대로 당신이 보여주고 싶지않은 버전의 스크립트를 보는 일이 없을 것입니다.

제가 도대체 뭘 한거죠?

마지막으로 한 commit으로 부터 어떤 변화가 있었는지 확인하기 위해서는:

$ git diff

어제부터 어떤 변화가 있었는지 확인하기 위해서는:

$ git diff "@{yesterday}"

어떤 특정 버전에서 부터 2번째 전 버전 사이의 변화를 확인하기 위해서는:

$ git diff 1b6d "master~2"

각각의 결과는 *git apply*와 함께 적용할 수 있는 패치가 될 것입니다. 다음 명령어도 사용해 보세요:

$ git whatchanged --since="2 weeks ago"

저는 윗 방법대신 qgit 를 따로 다운받아서 commit 히스토리를 체크하곤 합니다. 이 프로그램은 깨끗한 그래픽 인터페이스로 구성되어 있어보기 쉽지요. 아니면, tig, 텍스트형식 인터페이스 역시 느린 인터넷속도를 가지고 있는 분들에겐 도움이 될 것입니다. 또 다른 방법으로는 웹 서버를 설치한 후 *git instaweb*명령어를 사용하는 방법도 있겠지요.

연습

우선 A, B, C, D 를 각각 연속된 한 파일에 대한 commit이라고 가정합니다. 그리고 B는 A 에서 몇 개의 파일들이 삭제된 버전으로 가정합니다. 문제는 여기서 그 삭제된 파일들을 D에 더하고 싶을 때 어떻게 하는 것 인가 입니다.

적어도 세가지의 방법이 있습니다. 우선 우리가 현재 D에 있다고 생각합시다:

  1. A와 B의 차이점은 몇 개의 지워진 파일들 뿐입니다. 우리는 이 차이점을 패치로 따로 작성하여 본래의 디렉토리에 적용할 수 있습니다:

    $ git diff B A | git apply
  2. 우리는 A에 파일을 저장해 두었기에, 그 곳에서 다시 받아올 수 있겠지요:

    $ git checkout A foo.c bar.h
  3. 또는 A에서 B까지로 갈 때의 변화를 undo한다고 생각하셔도 됩니다:

    $ git revert B

어떤 방법이 가장 좋은 해답일까요? 답은 본인이 원하는 것이 곧 해답입니다. Git을 이용한다면 당신이 원하는 것은 쉽게 해낼 수 있고, 그 것을 해내는 방법은 한가지만 있는 것이 아닐겁니다.