이제 본격적으로 버전관리 도구인 git에 대해 배워 보도록 하겠습니다.
git을 본격적으로 사용해 보기 전에 기본적인 git의 구조와 원리를 조금은 이해하고 가 보기로 할까요?
git 기초
사실 모든 버전관리 시스템이 유사하기 때문에 기존 버전관리 도구들(cvs, subversion 등)을 사용해 보았다면 git의 구조나 원리가 어렵지 않습니다.
하지만 기존 버전관리 시스템을 사용해 본 경우라도 중앙 집중식 버전관리 시스템과 분산 버전관리 시스템이 서로 같지 않고 분산 버전관리 시스템 들 끼리도 내부적으로 사용되는 용어와 그 의미가 미묘하게 다르기 때문에 git을 제대로 사용하려면 우선 git이 무엇이고 어떻게 동작하는 지를 이해하는 것이 굉장히 중요합니다.
급한데로 가장 많이 쓰이는 명령어들, 이를 테면 clone, commit, push, pull 등등 만을 외워서 무작정 쓰기만 한다면 100의 보상을 받을 수 있는 도구를 가지고 10~20만 보상을 받게 되는 셈이 됩니다. (진짜 아이언맨 슈트를 구했는데 마스크 여닫는 것 밖에는 쓸 줄 모른단 이야기. 그리고 마스크 여닫는 게 아이언맨 슈트의 전부인 줄 착각한다는게 더 큰 문제.)
자 그럼 git의 핵심적인 원리와 구조를 알아보도록 하겠습니다.
스냅샷으로 버전을 관리하는 git
git이 가지는 기존 버전관리 시스템과의 가장 큰 차이점은 파일의 내용을 저장하고 버전을 부여하고 추적하는데에서 시작합니다. (당연하죠 버전관리 시스템 이니까요)
대부분의 기존 버전관리 시스템들은 변경된 파일의 목록정보와 각 파일의 내용중에 변경된 부분(델타라고 해요)을 저장하고 버전을 부여합니다.
버전 2 : File A가 이전 버전에서 델타1 만큼 변경되었고, File C가 이전 버전에서 델타1만큼 변경되었다.
버전 3 : File C가 이전 버전에서 델타2 만큼 변경되었다.
....
매 버전의 변경된 파일의 목록이 가장 근간이 되고, 목록상에 있는 파일들의 현재 버전은 이전 버전의 차이가 무엇이다 하는 정보로 저장합니다.
즉 직전 버전과의 차이가 가장 중요한 정보가 되지요. 사실 선형적인(변화가 시간의 흐름이라는 한 방향으로만 이루어 진다는) 전제에서는 당연한 거죠. 하지만 이런 선형적인 방식은 몇몇 단점을 가지게 되는데요. 예를들어 File C 의 버전 2와 버전 5가 내용이 동일하더라도 버전 2는 1과 달랐고, 버전 5는 4와 다르기 때문에 버전 저장소에는 버전 2와 버전 5의 파일이 중복 저장되게 됩니다.
이런 파일 내용의 중복 저장은 성능저하와 저장 용량 증가를 가져올 수 밖에 없구요.
또한 버전 5의 File C를 처리하려면 버전 1부터 시작해서 2,3 의 모든 변경 파일목록과 File C의 변경 이력을 모두 누적시켜야 하지요.
무엇보다 기존 버전의 중간에 다른 버전을 끼워 넣을수도 없고, 버전의 삭제도 최종 버전부터 순차적으로 삭제하는 방법밖에는 가능 하지 않습니다. 물론 이전 버전과의 이력을 단절시키고 Baseline 을 새로 그어서 다시 시작할 수 는 있지만요.
하지만 git은 위와 같이 이전 버전과의 차이를 저장하지 않고 현재 내 작업공간(내 PC상의 디렉토리라고 보시면 됩니다)의 상태를 그대로 저장(스냅샷)한 후 버전을 부여합니다.
이때 파일의 내용(예를 들어 File A 의 소스코드)을 버전 저장소에 저장되어 있는 파일의 내용과 비교(기존 버전과 비교하는 게 아니라 저장소에 저장되어 있는 파일의 모든 내용들과 비교합니다)하여 다를 경우에만 추가로 저장합니다. 같다면 참조링크 정보만을 저장하구요.
버전 2 : File A의 내용은 A1 이었다, File B의 내용은 B 였고, 이미 B 는 저장되어 있으니 추가 저장하지 않고 B 에 대한 링크만 저장.
버전 3 : File A의 내용은 A1 이니 참조링크만 저장, B도 참조링크만, C 의 내용은 C2 이고 없던 내용이니 새로 저장.
....
특정 시점(버전)에 각 파일이 가지고 있던 파일 내용이 가장 근간이 되며, 직전 버전과의 연관 없이 서로 독립적으로 존재(이전 버전을 참조할 수 있는 Link만 연결)하게 됩니다. 그림에 보이는 것처럼 File A 의 내용은 세 종류(A, A1, A2)이고, File B 의 내용은 네 종류(B, B1, B2) 이런식으로 저장해서 관리합니다.
따라서 버전을 아무리 증가시키더라도 기존에 저장되어 있는 파일의 내용(File A를 예로 든다면 A, A1, A2)와 동일한 내용(파일 내용의 비교는 SHA-1 알고리즘을 사용해서 매우작은 해쉬값 비교를 통해 이루어 집니다. SHA-1 알고리즘과 해쉬비교는 아래에 설명합니다)을 담고 있다면 파일을 추가로 저장하지 않고 파일 내용에 대한 참조Link만을 저장하여 버전을 관리하는 것입니다.
이러한 방식은 버전관리 속도면(현재 상태에 대한 스냅샷만 저장하면 되기 때문에 빠를 수 밖에요)에서나, 저장 용량면(동일한 내용을 담고 있는 파일은 중복 저장하지 않기 때문에 전체 버전관리에 소요되는 용량도 작아지죠)에서나, 중간 버전 끼워넣기나, 중간의 특정 버전 삭제, 버전 끼리의 병합 등 수많은 장점을 git에게 부여하게 됩니다.
특히나 수많은 브랜치를 동시에 생성하고 아무때나 해당 브랜치를 병합하고 다른 브랜치로 이전하는 등 순차적이지 않은 방식으로 개발할 수 있는 무한한 개방성을 부여해 줍니다. (브랜치도 긴 주제여서 별도의 포스트에 다루겠습니다.)
서버 저장소나 로컬 저장소나
git은 모든 정보를 서버가 아닌 로컬 저장소에서 저장하고 관리합니다. 서버와의 연결이 끊어져도 버전관리 작업에 아무런 제한이 없습니다.
git은 분산 버전 관리 시스템이기 때문에 서버에 있는 모든 버전 이력정보는 나의 로컬 저장소에도 모두 가지고 있습니다.
<Local Repository 는 Remote Repository 와 계급이 같아요. 주종 관계가 아님>
git은 최초 시작이 checkout 이 아니고 clone 인 점에 주목하시기 바랍니다.
git은 clone 을 통해 원격 저장소의 모든 정보를 내 장비의 로컬 저장소에 완전히 clone (똑같은 복사본, 최종 버전만이 아니라 저장소 DB 전체를 복사) 합니다.
Clone 을 통해 생성 된 Developer A 의 PC 에 있는 Local Repository 와 Developer B 의 PC 에 있는 Local Repository 와 원격 서버에 있는 Remote Repository 는 서로의 구분이 무의미한 동일한 저장소가 됩니다.
개발자 A 장비의 저장소가 개발자 B와 원격저장소 입장에서는 서버가 되고, 개발자 B 장비의 저장소가 개발자 A와 원격저장소 입장에서는 서버가 될 수 있습니다. 분산된 모든 저장소는 독립적으로 버전관리를 합니다.
원격 저장소의 존재 이유는 단지 나의 로컬 저장소를 외부에 열어두거나 다른 개발자에게 직접 제공하기가 불편하니 항상 접속이 가능한 네트워크 상의 경로에 나의 로컬 저장소의 사본을 필요할 때 복사해 두는 곳의 의미로 이해하는 것이 좋습니다.
결론적으로 나의 로컬 저장소에 이미 모든 정보를 가지고 있기 때문에 git은 파일을 꺼내고, 수정해서 버전부여하고, 동기화 하고 하는 거의 모든 버전관리 작업을 내 PC 에서 수행합니다. 내 PC가 인터넷에 연결이 되어있든 말든 상관없이 말이죠.
내 로컬저장소 정보를 누군가에게 공유해야 할 필요가 있을 경우에만 원격 저장소에다가 내 로컬 저장소 정보를 업로드 하는 것이지 원격저장소가 반드시 있어야만 하는 것이 아닙니다. 분산 버전 관리 시스템에서는 이를 이해하는 것이 무엇보다 중요합니다.
당장 이해가 어렵다면, git은 모든 이력정보를 내 로컬에도 저장하고 있기 때문에 모든 명령을 서버 접속없이 처리한다고 이해하시면 될 것 같습니다.
git의 무결성 관리
git은 데이터를 저장할 때 어떻게 파일의 내용을 비교해서 확인할까요?
git은 파일의 내용을 Hashing하여 결과값을 구하고 이 결과값을 비교하여 데이터를 관리합니다.
Hashing 이란 긴 내용을 줄여서 부를 수 있는 이름 짓기하기라고 할 수 있습니다. 모든 사람들에게 이름을 지어서 서로를 알아 볼 수 있는 것처럼 파일의 내용을 읽어서 서로 구분지을 수 있도록 짧지만 고유한 이름(Hashed Text)을 짓는 것이 Hasing입니다. 대신 보통의 이름짓기 처럼 임의로 짓는 것과 달리 Hashing은 본래의 내용을 읽어서 축약해서 만들기 때문에 같은 내용을 가지면 같은 이름을 같게 됩니다.
<Hashing을 통한 일방향 키값 생성>
예를 들어 자동차가 있는데 창문이 4개 있으면 4W, 2인승이면 2P, 전륜구동이면 F, 후륜구동이면 R 이런식으로 같은 특징에는 항상 같은 이름이 부여되어 차종이 4W2PF, 3W4PR 이런식으로 구분되어 지는 것과 비슷하다 하겠습니다.
컴퓨터에서는 파일의 저장된 데이터를 읽어서 이보다 조금은 복잡한 규칙들을 통해 복사가 거의 불가능한 유일한 이름을 부여하는 것을 Hashing이라고 합니다.
Hasing의 특징은 Hashing결과값을 통해서는 원본을 만들어 낼 수 없지만, 원본이 동일하다면 항상 같은 Hashing 결과값이 계산되는 일방향성입니다. 이런 일방향성으로 인해 보안, 검증, 압축 등에 다양하게 활용되는 것이 Hashing이고 이런 Hashing을 더 빠르게 더 안전하게 구현하기 위한 다양한 알고리즘이 이미 세상에는 많이 구현되어 있습니다.
<출처 : 요즘 핫한 비트코인의 Hashing 알고리즘>
git은 이런 Hashing알고리즘 중에 SHA-1 이라는 알고리즘을 사용하는데 SHA-1 으로 생성된 키값은 알파벳 40글자가 됩니다. (아무리 큰 파일이라도 40자로 키값이 만들어짐)
git을 사용하는 중에 계속 보게되는 수많은 정체를 알 수 없는 알파벳 문자열들은 모두 이 Hashing결과로 생성된 키값이라는 것을 쉽게 짐작하실 수 있겠죠?
Hashing 키 값은 원본이 없다면 동일한 키 값을 유추해 낼 수가 없기 때문에 임의의 문자열 조합을 만들어내서 git내부의 파일에 대해 임의로 접근하거나 조작하는 것이 불가능 하도록 보호하는 장점도 갖게 합니다.
오로지 더하기
git은 어떤 버전관련 작업을 수행 하더라도 더하기를 통해 처리합니다.
버전관리라는 것이 새로운 버전 정보를 추가하는 작업이니 당연한 건데 이게 왜 git의 특징이냐구요?
버전관리 작업 중에는 revert 라고 하는 작업이 있습니다. revert는 특정 버전의 파일을 저장소에서 꺼내(checkout) 온 후 열심히 수정을 한 다음에 버전을 만들었는데, 수정한 내용이 마음에 요구사항에 맞지 않거나 구현이 잘못되어서 고치는 것 보다는 지금까지 수정한 내용을 취소하고 파일의 내용을 수정하기 전의 버전으로 되돌리는 작업입니다.
다른 버전관리 도구들은 이 revert를 수행하면 최종 버전이나 임시로 만들어진 버전을 삭제시키고 직전의 버전으로 되돌아 가지요.
하지만 git은 revert를 수행하면, 현재의 버전을 삭제하지 않고 현재 버전 직전의 버전을 가져와서 현재 버전의 뒤에 다음 버전으로 commit 시켜 버전을 증가(더하기)시키는 것으로 파일의 내용을 수정하기 전의 모습으로 되돌립니다.
<git은 revert 도 commit 임!>
어떤 이유에서던 수정했던 작업을 잃어버리지 않도록 하는 것이죠.
git에 익숙해지면, 정말 특정 버전의 파일을 오히려 날려버리기가 더 어려워져서 맘껏 버전을 만들고, 지우고, 옮기고, 합치고, 되돌리고 해 볼 수 있습니다. 익숙해 지면요.
Working, Staging, Repository 구분
git은 모든 파일을 working, staging, repository 의 세 가지 단계(상태?)로 관리합니다.
내가 지금 소스코드를 작성하고 고치고 하는 작업공간인 working directory 가 있습니다.
working directory 에 있는 수정된 파일들 중에 repository 에 올리려고 임시로 적재(git add)시켜 두는 staging area 가 있습니다.
임시로 적재된 파일을 실제 버전 관리를 위해 업로드(commit) 하면 최종 저장되는 곳인 repository 가 있습니다.
<working 에서 선택된 파일만이 staging 으로 올라가고, staging 에 올라온 파일은 commit 으로 repository 로 저장>
IDE를 통해 버전관리를 그냥 하다보면 이를 이해할 기회 자체를 놓치는 경우가 많습니다. IDE가 staging 으로 넘겼다가 repository 로 넘기는 걸 다 알아서 해주니까요. 하지만 git에서는 이 3가지 단계를 이해하는 것이 매우 중요합니다.
그런데 물리적으로 같은 디렉토리에서 git은 어떻게 같은 디렉토리의 파일들을 이렇게 3가지로 구분할 수 있을까요?
git은 clone을 해서 원격저장소를 복사해 오거나, init을 해서 초기저장소를 만들면 .git이라는 경로는 만드는데, 이 .git경로에 프로젝트의 모든 메타데이터(히스토리, 브랜치, staging정보 등)와 모든 객체 데이터베이스(버전, 파일데이터, 커밋메세지 등)를 저장하여 이 데이터베이스를 통해서 git은 모든 버전관리 작업을 수행하게 됩니다.
.git 경로는 숨겨져 있구요. 이를 삭제하면 git의 모든 것을 잃어버리게 됩니다.
원격저장소에 올라가는 것도 이 .git경로이구요. .git경로를 다른 원격저장소에 한번도 올리지 않았는데 이 .git경로를 지워버리게 되면 모두다 날려버리게 되니 주의하시기 바랍니다. (.git경로가 지워지더라도 최종작업을 하던 working directory는 유지가 되니 모두 날리는 것은 아니지만..)
working direcotry 는 .git 경로의 데이터베이스에서 특정 버전을 복사해서 꺼내온(checkout) 것입니다. git은 .git디렉토리에서 해당 버전의 압축된 원본을 가져와서 working directory에 풀어서 working directoy를 만들어 냅니다. 이후에 이 working direcotory에 있는 소스 파일들을 OS가 인식하고, 우리도 에디터 프로그램을 통해서 working directory에 있는 이 소스파일들을 수정하게 되는 것이구요.
staging area 는 .git 경로에서 관리되는 가상의 공간(OS에서는 볼 수 없고 git client를 통해서만 조회할 수 있음)이라고 할 수 있습니다. 실제로는 단순한 파일로 작성되어 있고, 커밋예정인 파일에 대한 정보를 저장하고 관리합니다. 파일로 작성되어 있기 때문에 'index' 라고 불리기도 합니다.
repository 는 .git 경로에 저장되어 있는 역시 가상의 공간입니다. OS의 탐색기를 통해서 접근하면 그냥 의미를 알수 없는 경로와 파일들 뿐이지만 git client를 통해 접근하면 모든 버전과 브랜치와 이력과 파일내용을 확인하고 버전관리 작업을 수행할 수 있도록 참조되는 곳이 이곳입니다.
.git 디렉토리에 저장되어 있는 파일은 Commited 상태를 가집니다.
파일을 수정하고 Staging 에 add 한 파일은 Staged 상태를 가집니다.
파일을 수정했지만 아직 Staging 에 add 하지 않은 파일은 Modified 상태를 가집니다.
파일 상태에 대한 보다 상세한 내용은 추후 다른 post 를 통해 설명하도록 하고 오늘 git의 기본구조와 특징에 대한 긴 이야기는 이만 마치겠습니다. 긴글 읽어주셔서 감사합니다.
:: 다음에 계속됩니다.
'Development Tools > Git' 카테고리의 다른 글
git 시작하기 (4) - git cli 설치 (0) | 2017.11.19 |
---|---|
git 시작하기 (2) - git의 역사 (0) | 2017.11.18 |
git 시작하기 (1) - 버전 관리란? (0) | 2017.11.17 |