1장. 도입부

저는 비유법을 사용하여 버전 관리 시스템에 대하여 설명해보려 합니다. http://en.wikipedia.org/wiki/Revision_control 를 방문하셔서 덜 정신나간 버전의 설명을 보시길 권장합니다.

일하는 것은 곧 노는 것

저는 거의 평생을 컴퓨터게임을 하며 지냈습니다. 하지만 어른이 되서야 버전 관리 시스템을 사용하기 시작했지요. 이런 건 저 혼자가 아닐 것이라 생각합니다. 그럼으로써 Git을 게임에 비유하며 설명하는 것이 Git을 이해하는 데 도움이 될 것이라 생각합니다.

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

그러나 이러한 행동은 예전 세이브를 덮어쓰는 결과를 초래하죠. 세이브 슬롯이 한 개밖에 없는 옛날 구형 게임을 생각하면 됩니다. 다시 말하자면, 세이브를 할 수는 있지만, 당신은 예전 세이브포인트로 돌아갈 수 없는 것 입니다. 이건 참…게임을 아주 재미있는 순간에 세이브를 해 놓았는데 돌아갈 수 없다는 것이죠. 더 나쁜 상황으로는, 당신의 세이브는 더 이상 이길 수없는 상태에 되어 있을 수도 있습니다. 그럴 경우에는 아주 처음부터 다시 시작해야 되겠지요.

버전 관리

편집 시, 다른 이름으로 저장 기능 및 사본을 다른 디렉토리에 만들어 놓는 방법 등을 이용해 오래된 버전을 보존할 수는 있습니다. 용량을 효율적으로 사용하기 위해서 압축을 할 수도 있죠. 이것은 참 원시적인 버전 컨트롤 방법입니다. 컴퓨터게임은 이런 과정에서 발전해 나간지 오래되었지요. 요즘 게임들은 여러개의 세이브 슬롯을 잘 활용하고 있습니다.

이 문제를 좀 더 꼬아서 보죠. 어떤 프로젝트나 웹사이트를 구성하는 소스코드와 같이 여러개의 파일이 하나로 묶여있다고 가정합시다. 현 버전의 프로젝트/웹사이트를 세이브하고 싶다면 모든 디렉토리를 기록해야 한다는 번거로움이 있지요. 일일이 많은 버전들을 관리한다는 것은 그리 효율적이지 않을 겁니다.

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

버전 관리 시스템은 이런 컨셉과 그리 다르지 않습니다. 버전 관리 시스템들은 디렉토리 등을 관리하기에 편한 인터페이스로 구성되어 있습니다. 원하는 만큼 세이브 및 불러오기를 실행할 수 있습니다. 컴퓨터게임들과는 다르게 용량을 효율적으로 사용하는 데에는 탁월한 성능을 보여주죠. 대부분의 케이스는 소수의 파일들만 살짝 편집을 하게되죠. 디렉토리 전체를 세이브하는 것 보다는 버전과 버전사이의 차이를 세이브하는 것이 용량을 효율적으로 쓰는 버전 컨트롤의 비밀입니다.

분산 제어

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

어떻게 시스템을 구축해 두어야 게이머들이 서로의 세이브파일들을 올리거나 이어 받을 수 있을까요?

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

만약에 어떠한 이유에서라도 한 게이머가 예전에 세이브 해두었던 파일을 불러오고 싶다면 어떻게 될까요? 현재 세이브 시점은 아무리 잘 해도 이길 수 없는 상태로 저장이 되어있을지도 모르고, 게이머들은 현 시점보다 전으로 돌아가서 아까 못 구했던 강력한 아이템을 얻을 수 있는 시점으로 돌아가고 싶을지도 모릅니다. 그런게 아니라면 그들은 아마도 세이브파일 두 개를 비교하여 한 특정 게이머가 얼마나 진행을 해두었는지 알고 싶어할지도 모릅니다.

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

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

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

멍청한 미신

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

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

규모가 작은 프로젝트는 어떤 시스템으로부터 부분적인 특성만이 필요할 지 모르지만, 규모가 작은 프로젝트를 잘못 스케일링하는 시스템은 마치 로마 숫자를 이용해 작은 숫자들의 계산을 실행하는 것과 같다.

더욱이 당신의 프로젝트는 당신이 생각했던 것보다 더 큰 일이 될지도 모릅니다. 처음부터 Git을 사용한다는 것은 아무리 병뚜껑을 여는 데만 쓴다하여도 스위스아미나이프를 들고 다니는 것과 같은 것입니다. 드라이버가 필요할 경우 당신은 병따개를 들고다니지 않았다는 사실에 안도의 한 숨을 쉴 수 있을 것 입니다.

결합의 오류

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

앨리스는 파일 편집 중 첫 줄에 새로운 줄을 추가하고, 밥은 그 파일의 마지막에 한 줄을 넣는다고 가정합니다. 그들은 편집된 파일을 업로드 합니다. 대부분의 시스템은 자동으로 두 사람이 한 편집을 받아들이고 병합할 것입니다. 결과적으로는 앨리스와 밥 두 사람의 편집이 적용될 것입니다.

자 이제 앨리스와 밥이 같은 부분에서 서로 다른 편집을 한다고 가정해 봅니다. 인간의 직접적인 개입없이는 불가능 하겠지요. 누가 편집을하던 두번째로 편집하는 사람은 merge conflict라는 메세지를 볼 수밖에 없겠지요. 한 사람만의 편집을 선택하거나 두 사람의 편집과는 다른 세번째 편집을 생각해 봐야 할 겁니다.

더 복잡한 상황이 일어날 수 있습니다. 버전 관리 시스템은 간단한 상황들을 알아서 해결해 줍니다. 어려운 상황은 인간의 손에 맡기지요. 시스템의 행동은 대체적으로 조정가능합니다.

==기본적인 요령==

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 add readme.txt Documentation

그리고 만약에 원하지 않는 파일을 Git에서 없애려면:

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

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

파일 이름바꾸기는 원치않는 현재의 이름을 지우고 새로운 이름을 새롭게 지정하는 컨셉과 같습니다. 좀 더 손쉬운 방법으로는 git mv 명령어가 있습니다. 예를 들어:

$ git mv bug.c feature.c

고급 undo와 redo

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

$ git log

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

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

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

Hash를 자르고 붙여넣기 싫으시다고요? 그렇다면:

$ git checkout :/"My first b"

이 명령어를 사용함으로써 이 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 에 들어가서 히스토리를 체크하곤 합니다. 이 웹사이트는 깨끗한 그래픽 인터페이스로 구성되어 있어 보기 쉽지요. 아니면, 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을 이용한다면 당신이 원하는 것은 쉽게 해낼 수 있고, 그 것을 해내는 방법은 한가지만 있는 것이 아닐 겁니다.