들어가며
git이란 무엇일까? git의 역사부터 github란 뭔지에 대해서까지 포스팅할 예정이다. 이번 포스팅은 개발을 입문하는 사람에게 초점이 맞춰줘 있다.
본론
git의 역사
git의 역사를 이야기하면 리눅스의 이야기도 빼놓을 수 없다. 깃이라고 하는 VSC(버전 관리 시스템)은 리눅스와 함께 개발되었다.
Linux 커널은 굉장히 규모가 큰 오픈소스 프로젝트다. 이 커널 프로젝트는 당연히 오랫동안 진행되었는데, 1991-2002년 사이에는 Patch와 단순 압축 파일로만 관리했다. 2002년에 드디어 Linux 커널은 BitKeeper라고 불리는 상용 DVCS(분산 버전 관리 시스템)를 사용하기 시작했다.
하지만 2005년에 커뮤니티가 만드는 Linux 커널과 이익을 추구하는 회사가 개발한 BitKeeper의 관계는 틀어졌는데, 비트키퍼에서 무료 사용을 더 이상 금지하는 것이다. Linux 자체는 커뮤니티에서 개발하는 오픈소스였기 때문에, VCS 오픈소스 개발이라는 주제를 리눅스 개발 커뮤니티에서 떠안게 된 것이다. 그렇게 오픈 소스로 git이라는 이름의 VCS가 개발되었다. 창시자는 리눅스의 개발자인 리눅스 토발즈이다.
git은 BitKeeper를 사용하면서 배운 교훈을 기초로 아래와 같은 목표를 세웠다.
- 빠른 속도
- 단순한 구조
- 비선형적인 개발(수천 개의 동시 다발적인 브랜치)
- 완벽한 분산
- Linux 커널 같은 대형 프로젝트에도 유용할 것(속도나 데이터 크기 면에서)
Git은 2005년에 그렇게 탄생하여 아직도 초기 목표를 그대로 유지하고 있다. 현대에는 이러한 git을 기반으로 다양한 서비스들이 git을 쉽게 사용할 수 있게 진화하여 github, gitlab 등 다양한 툴들이 나오는 등 git은 개발자들에게 빠질 수 없는 요소가 되었다.
위 목표를 생각하고 git을 사용한다면 2000년대 초반에 나왔던 위 아이디어가 얼마나 훌륭한 생각이었는 지 알 수 있다.
git이란?
먼저 git은 VCS(version controll system) 중 하나이다. 이 말은 git 외에도 다른 버전 관리 시스템이 많다는 뜻인데, 예시로는 CVS, Subversion, Perforce 등이 있다. 이러한 다른 VCS과 함께 git을 비교하며 좀 더 쉽게 이해해 보자.
변경 사항이 아니라 스냅샷을 사용
git과 다른 VCS의 가장 큰 차이점은 git이 데이터를 생각하는 방식에 대한 것이다. 개념적으로 대부분의 다른 시스템은 정보를 파일 기반 변경 목록으로 저장한다.
이 사진을 보면 알 수 있겠지만, 버전 관리를 할 때 이전 버전에 비해 변경된 부분을 기록하는 것이 다른 VCS들이다. ( git은 이렇지 않다 ) 이러한 것들의 예시로는 CVS, Subversion, Perforce 등의 VCS들이 있는데, 이를 델타 기반 버전 제어라고 부르기도 한다. 그렇다면 이제 이러한 델타 기반 버전 제어와 다른 git의 특별한 버전 관리는 무엇일까?
git은 데이터를 이런 식으로 생각하거나 저장하지 않는 대신, git은 데이터를 일련의 스냅샷처럼 생각한다. git을 커밋하면 그 순간 모든 파일이 그 순간에 어떻게 보이는지 사진을 찍고 해당 스냅샷에 대한 참조를 저장한다. 효율을 높이기 위해서 변경되지 않은 부분은 git은 그 파일을 다시 저장하지 않고, 이미 저장한 동일한 파일에 대한 링크만 저장한다. 이는 $ git status 명령어를 통해 git이 어떤 파일을 track 하는지 조사를 통해 알 수 있다. ( 자세한 내용은 밑에서 다룬다 ) git은 데이터를 스냅샷의 스트림처럼 생각한다. 다음과 같이 말이다.
스냅샷으로 버전을 관리하는 것은 거의 모든 다른 VCS 간의 중요한 차이점이라고 할 수 있다. 이렇게 생각하면 git은 VCS라기보다는 미니 파일시스템에 가깝다고 생각할 수 있다. 이러한 스냅샷 방식의 이점이 있을까?
공식 문서에 따르면 다양한 이점이 있다고 한다. 우리가 왜 git을 사랑하는지 그 이유를 자세히 알아보자.
로컬 작업
첫 번째 이점은 로컬 작업이다. git의 대부분의 작업은 로컬 파일과 리소스만 있으면 작동하고 일반적으로는 네트워크가 없이도 작동한다. 따라서 로컬에서도 개발할 수 있다는 것이다. 무슨 뜻인지 풀어서 설명하면, local 환경에서 디렉터리 내에 존재하는 .git 파일이 해당 디렉터리 내에 모든 변화를 감지한다는 것이다.
예를 들어, 위 사진처럼 hyeongjun 디렉터리는 .git이 있다. $ git init 명령어를 통해 해당 디렉터리에 .git을 생성할 수 있다. 이렇게 .git 파일이 있다면 해당 디렉터리의 모든 변화를 감지하여 반영하게 된다. 이러한 이유로 깃은 네트워크가 없이도 동작하는 것이다.
위 상태는 아무런 변화가 없는 상황이다. 여기서 간단하게 vi 명령어를 통해서 test.py 파일의 내용을 변경하게 되면 어떤 일이 일어날까?
이렇게 간단하게 test.py를 변경하고 나서 $ git status라는 명령어를 치게 되면, 깃이 변경된 파일을 감지한다. test.py라는 파일이 modified 되어 있다는 것이다. 추가로 다양한 명령어를 통해서 commit 하고 나서 push 하라고 친절하게 쓰여 있는데, 이러한 변경 사항을 아래와 같은 명령어를 통해서 저장하는 것이다.
git add test.py
git commit -m "간단한 변경"
# github와 같은 원격 저장소로 .git의 내용을 동기화하는 경우
git push
git commit을 하게 되면 디렉터리에 있는 .git에 변경 내용이 저장된다. 이 상황에서 git push 명령어를 사용하게 되면 .git에 기본적으로 저장된 원격 저장소 ( 깃허브와 같은 곳 ) 으로 .git을 동기화하게 된다. 이런 상황에서는 당연히 네트워크 연결이 필요하다.
git의 무결성
두 번째 깃의 장점은 무결성이다. git의 모든 항목은 저장되기 전에 체크섬(checksum) 과정을 거친다. 파일이 변경되면 해시 함수를 통해서 40자의 해시값을 얻고, 이를 비교하여 변경사항이 있는지 감지하는 것이다. 이러한 체크섬을 사용함으로써 파일이나 디렉터리가 변경되었는지, 손상되었는 지 확인할 수 있게 된다. 이러한 장점이 git의 무결성인 것이다.
git이 이 체크섬을 위해 사용하는 해시 함수를 SHA-1 해시라고 한다. 이는 16진수 문자로 구성된 40자 문자열로, git의 파일 또는 디렉토리 구조를 내용을 기반으로 저장된다. git을 사용하다 보면 아래와 같은 값을 자주 보게 된다.
- 예시 : 24b9da6552252987aa493b52f8696cd6d3b00373
실제로 git은 데이터베이스에 모든 것을 파일 이름이 아닌 콘텐츠의 해시값으로 저장한다. 그렇다면 이 해시값으로 버전을 관리한다는 것일까? 사실이다.
$ git log 명령어를 통해서 위와 같이 디렉터리를 변화시켰다. 간단한 파일을 추가한 것인데, 모든 커밋에 해시값이 남아있다. 이러한 해시값을 통해서 git reset 명령어를 통해 해당 커밋의 스냅샷으로 돌아갈 수 있다. 이는 굉장히 강력한 기능이다!!
git의 세 가지 유형
git이 코드를 관리할 때, 코드는 크게 세 가지 유형으로 나눌 수 있다.
- modified : 파일을 변경했지만 아직 git 데이터베이스에 커밋되지 않았음을 의미
- staged : 수정된 파일을 현재 버전으로 표시하여 다음 커밋 스냅샷으로 이동하는 것을 의미
- committed : 데이터가 로컬 데이터베이스에 안전하게 저장되었음을 의미
코드를 변경하면 modified 상태가 되고, git add 명령어를 통해 이를 staged 하게 만든다. 마지막으로 commit 명령어를 통해 .git 데이터베이스에 스냅샷이 반영되게 된다.
이러한 과정을 통해서 우리가 작업을 하는 작업 디렉터리에 있는 .git이 이러한 것들을 감지하고, 변경된 사항을 저장하게 된다. 이렇게 local에서 작업된 코드를 원격 저장소에 push 함으로써 다 같이 코드를 공유하고, 함께 작업할 수 있게 되는데 이번에는 github에 대해서 알아보자.
git push 그리고 github
git push 명령어는 크게 다루지 않았는데, push 명령어를 사용하면 원격 저장소와 .git의 내용을 동기화한다. 이것이 오픈 소스나 협업의 시작점인데, 가장 일반적으로는 깃허브에 코드를 올리는 것이다. 깃허브에서 레포지토리를 만들어 git clone 명령어를 사용하면 자동으로 원격 저장소가 깃허브가 되어 $ git push 명령어를 통해 알아서 깃허브에 코드를 올릴 수 있다.
하지만 $ git init을 통해서 .git을 만들고 이를 깃허브 레포지토리에 푸시하려고 하면 안 될 것이다. ( remote 등록이 안 되어있기 때문 ) $ git remote 명령어를 통해서 원격 저장소를 지정해 준 후 $ git push 명령어를 통해서 로컬의 .git 파일의 내용을 원격 저장소와 동기화할 수 있다.
git 코드 원격 저장소로 github 외에도 다른 플랫폼이 많지만, 오늘은 가장 대중적인 github를 사용한다는 가정 하에 포스팅하려고 한다.
github
오픈 소스의 성지 깃허브이다. 2008년 깃허브도 재미있는 이슈로 만들어지게 되었고, 사용자 친화적인 인터페이스와 무료 공개 레포지토리 등을 통해서 사용자들에게 큰 인기를 얻었고 현재까지도 개발자의 상징으로 자리매김하게 되었다.
깃허브라는 이름답게 git을 모아주는 역할도 하고, 이에 추가로 다양한 협업 기능을 추가했다. 풀 리퀘스트(Pull Request), 코드 리뷰, 위키, 이슈 등 다양한 협업 기능을 추가함으로써 깃허브는 개발자의 필수 덕목이 된 것이다.
깃허브를 통한 협업
백엔드 node 프레임워크인 nestjs의 github organization이다. 이와 같이 깃허브는 다양한 협업 기능을 제공하여 오픈 소스의 성지가 됐다. 이렇게 다양한 사람들이 프레임워크를 개발한다고 생각해 보자. 누군가 겹치는 코드를 올리거나 할 수 있지 않을까? 그럴 때에는 어떻게 처리할 수 있을까?
git branch
깃에는 브랜치(branch)라고 하는 개념이 있다. 협업을 하기 전에 반드시 알아야 하는 개념이다. 브랜치 개념을 알고 있다면 수많은 사람들이 자기의 로컬 환경에서 개발한 소스 코드를 시도 때도 없이 깃허브에 업로드하는 데에도 문제없이 서비스가 굴러가는 것을 이해할 수 있을 것이다.
먼저 상황을 가정해 보자. 기본 브랜치(master)에서 작업을 하여, 커밋이 계속 쌓이고 있는 상황이다. 여기서 $ git branch testing 명령어를 통해 testing이라는 브랜치를 만들 수 있다. 참고로, master는 예전에 깃에서 사용하는 default 브랜치 명칭이고, 지금은 main을 디폴트로 사용하도록 바뀌었다.
git branch 명령어를 통해 다음과 같은 브랜치를 만들었다. 깃에는 HEAD라고 하는 포인터가 존재하는데, 우리가 디렉터리의 코드들과 현재 어떤 시점을 바라보고 있는 지를 가리키는 것이 HEAD이다. 아래 사진은 master 브랜치를 가리키는 경우다.
여기서 git checkout 명령어를 통해서 현재 바라보고 있는 브랜치를 testing으로 옮길 수 있다. 위 사진에서는 HEAD가 master 브랜치를 포인팅 하고 있기 때문에 master 브랜치에 있는 상황이지만, $ git checkout testing 명령어를 통해서 브랜치를 옮길 수 있다. 그러면 아래와 같이 포인팅 된다.
이렇게 testing 브랜치에서 작업을 진행하고 나서, 변경된 파일을 커밋하게 되면 어떻게 될까? 브랜치가 갈라지게 된다. 아래와 같이 브랜치가 나뉜다.
현재 testing이 보고 있는 파일은 87ab2 시점이다. 다시 checkout 명령어를 통해서 master 브랜치로 넘어오게 된다면 testing이 보고 있는 시점의 한 단계 뒤로 돌아오게 된다.
여기서 다시 master 브랜치에서 작업하고 커밋하게 되면 어떻게 될까? 브랜치가 다음과 같이 나뉘게 된다.
혼란스러운 상황이다. 하지만 이렇게 브랜치가 나뉘는 것이 깃을 통한 협업의 시작인데, 이렇게 master 브랜치 ( 지금은 main 브랜치를 쓴다 ) 에서 브랜치를 파생하여 작업을 시작하는 것이다. 깃허브에서 보게 되는 코드는 모두 master 브랜치를 나타내고 있고, 많은 사람들이 작업하는 것을 확인하면 master 브랜치에서 파생된 브랜치에서 작업을 하고, 그 변화된 브랜치의 내용을 master으로 병합(merge)하는 방법을 사용한다.
nestjs라는 오픈 소스 프레임워크의 swagger 라이브러리에는 위와 같은 브랜치들이 존재한다. master 브랜치에서 직접적으로 작업하지 않고, 다른 브랜치에서 작업을 한 후에 메인 브랜치(여기서는 마스터 브랜치)로 합치려는 요청을 보내 함께 협업을 할 수 있게 된다.
위 레포지토리의 Pull Request 섹션에 들어가면 이러한 요청들을 볼 수 있다. 자신의 브랜치에서 작업한 내용을 메인 브랜치로 합병(merge)하고 싶다는 요청을 위와 같이 보낼 수 있다. 이러한 풀 리퀘스트( 줄여서 PR )이라는 과정을 통해서 다양한 사람들이 깃허브에서 협업을 하고, 서비스를 완성한다.
git flow
그렇다면 브랜치를 팔 때, 어떤 식으로 판다는 말인가? 그냥 아무렇게나 만들면 될까? 이러한 질문에 대한 답변으로는 git flow가 있을 수 있다. 이는 회사마다 혹은 오픈소스마다 다른 git flow 전략을 가지고 있지만 보통 다음과 같이 브랜치를 분리하여 작업한다.
이러한 방식으로 master 브랜치에서 관심사를 분리하여 브랜치를 나누고, 다양한 작업을 합쳐가면서 git을 협업 도구로써 사용하게 된다.
commit convention
브랜치를 파는 방법은 위와 같이 설명했다. 브랜치의 convention(관습)에 대해서 설명했으니 이번에는 commit convention의 차례이다. 이 또한 브랜치와 마찬가지로 회사마다 혹은 오픈 소스마다 다른 커밋 컨밴션을 가지고 있다. 보통은 다음과 같은 컨밴션을 지킨다.
$ git commit -m "feat: add new login feature"
$ git commit -m "fix: correct minor typo in code"
이와 같이 커밋 메시지를 간결하고, 이해하기 쉽게 작성하여 다른 개발자들이 쉽게 코드 베이스를 파악할 수 있게 지키는 관습이다. 구조는 "타입(type): 설명(descriptiom)"의 구조를 가진다. 타입 뒤에 바로 :(콜론)을 붙이며 다음에 바로 띄어쓰기가 온다.
타입 이름 | 설명 |
feat | 새로운 기능을 추가할 때 사용 |
fix | 버그를 수정할 때 사용 |
chore | 코드 수정, 내부 스크립트, 라이브러리 업데이트 등 프로덕션 코드를 건들지 않는 유지보수 작업에 사용 |
docs | 문서 변경사항에 사용 ( README, Swagger 등 ) |
style | 코드의 형식/스타일 변경에 사용 ( 동작에 영향을 미치지 않는 변경 ) |
refactor | 코드를 리팩토링할 때 사용 |
test | 테스트 추가나 기존 테스트 코드 수정에 사용 |
ci | CI(Continuous Integration) 시스템과 관련된 변경사항에 사용 |
build | 빌드 프로세스 또는 이부 종속성과 관련된 변경사항에 사용 |
revert | 이전의 커밋을 되돌릴 때 사용 |
자주 사용하는 커밋 컨벤션의 타입은 위 표와 같다. 이러한 내용을 명시하여 커밋 메시지를 작성하면 커밋 메시지만 보고서 대충 어떤 작업이 오고 갔는지 쉽게 파악할 수 있다.
간단하게 이런 식으로 Commits의 리스트만을 보는 것만으로도 쉽게 무슨 일이 일어났는지 알 수 있다.
마지막으로는 이렇게 다른 브랜치에서 작업한 자신의 코드를 메인 브랜치로 병합하기 위해 요청하는 (PR)Pull Request 작업이 있다. PR 요청을 통해 다른 개발자들의 코드 리뷰를 받을 수 있다. 다른 개발자들의 approve 승인을 받아 소스 코드를 메인 브랜치로 병합함으로써 깃허브를 통해 프로덕트를 개발할 수 있다.
마치며
코드 리뷰하는 사람들의 시간도 소중하니 PR과 commit mesagge는 간결하고 명확하게 작성해줍시다.
참고
- [git-scm.com] git : https://git-scm.com/book/en/v2/Getting-Started-What-is-Git%3F
감사합니다.
'Developer' 카테고리의 다른 글
vi 명령어 및 설정 (1) | 2023.12.11 |
---|---|
인증과 인가에 대해서 (2) | 2023.10.02 |
Webhook에 대해서 (1) | 2023.10.01 |