키워드로 알아보는 git 기초

Aaron·2020년 12월 31일
0

git

목록 보기
1/1
post-thumbnail

버전 관리 시스템


버전 관리 시스템이란?

파일 변화를 시간에 따라 기록했다가 나중에 특점 시점의 버전을 다시 꺼내올 수 있는 시스템.

분산 버전 관리 시스템 (DVCS)

모든 프로젝트는 공식 저장소로 간주되는 중앙 저장소가 있으며 개발자들은 이를 히스토리와 더불어 전부 복제(clone)할 수 있다.
따라서 중앙집중식 버전 관리(CVCS)와 다르게 서버에 문제가 생기면 이 복제물로 다시 작업을 시작할 수 있다. 클라이언트 중 어떤 것을 골라도 복원할 수 있다. clone은 모든 데이터를 가진 진정한 백업인 것이다!

아래와 같은 형태로 작업이 많이 이루어진다.
a. 중앙 원격 저장소 clone -> 로컬 저장소에서 작업 -> 중앙 원격 저장소로 push
b. 중앙 원격 저장소 fork하여 자신의 원격 저장소로 가져오기 -> fork로 가져온 자신의 원격 저장소를 clone하여 로컬 저장소에서 작업 후 그대로 push -> fork한 자신의 원격 저장소에서 중앙 원격 저장소로 pull request (Github 한정)

깃잘알 동료께서 위 사례 중 b는 Github 한정(Fork와 pull request는 Git의 용어가 아니라 Github의 용어) 이라고 명시하면 좋을 것 같다는 피드백을 주셨습니다. 다른 버전 관리 도구에서는 방법이 다를 수 있음을 알립니다. 가르침을 주셔서 감사합니다.

Git의 역사


Linux 커널은 방대한 규모의 오픈소스 프로젝트이다. patch와 단순 압축 파일로 관리하던 시절(1991-2002)을 벗어나 2002년부터 상용 DVCS인 BitKeeper를 사용하기 시작했다.
그러나 2005년 BitKeeper의 무료 사용이 재고되면서 Linus Torvalds를 필두로 한 Linux 개발 커뮤니티는 자체 도구를 만들어 쓰기로 했다. 도구를 만들 때 세운 목표는 아래와 같다.

  • 빠른 속도
  • 단순한 구조
  • 비선형적인 개발 (수천 개의 동시다발적인 브랜치)
  • 완벽한 분산
  • Linux 커널 같은 대형 프로젝트에도 유용할 것 (속도나 데이터 크기 측면에서)

로컬 기반


거의 모든 명령이 로컬 파일과 데이터만 사용하기에 네트워크를 거칠 필요가 없어 엄청난 퍼포먼스를 자랑한다.
프로젝트의 모든 히스토리가 로컬 디스크에 있기 때문에 모든 명령이 순식간에 실행된다.
어떤 파일의 현재 버전과 한 달 전의 상태를 비교하고 싶을 때도 Git은 그냥 한 달 전의 파일 히스토리와 지금의 파일을 로컬에서 찾는다. 파일 비교를 위해 네트워크를 거쳐 서버에 접근해서 예전 버전을 가져올 필요가 없다.
또한 오프라인 상태여도 막힘없이 작업이 가능하다. 비행기나 기차 등 네트워크에 접속되지 않은 상태에서 작업하고 커밋할 수 있다. (CVCS에서는 네트워크에 접속되지 않은 상태면 저장소에 접근이 불가능하므로 편집은 가능해도 커밋은 할 수 없다.)

스냅샷


스냅샷이란?

특정 시간에 데이터 저장 장치의 상태를 별도의 파일이나 이미지로 저장하는 기술.

SubVersion(SVN) vs Git 핵심 비교

SubVersion

출처: git-scm의 pro git book Chapter 1.3

  • 파일의 변화(차이점)를 저장.
  • 파일의 변화를 시간순으로 관리.
  • Version 5를 가져오는 시나리오 -> 기초가 되는 File A, B, C와 함께 모든 변경 내역을 네트워크를 거쳐 서버로부터 내려받는다. (CVCS니까)

Git

출처: git-scm의 pro git book Chapter 1.3

  • 스냅샷으로 저장
  • 데이터를 스냅샷의 연속(스트림)으로 취급.
  • 파일이 달라지지 않았으면 Git은 성능을 위해 똑같은 파일을 새로 저장하지 않는다. 이전 상태의 파일에 대한 링크만 저장한다. (위 이미지 상의 점선 테두리로 둘러싸인 부분)
  • Version 5를 가져오는 시나리오 -> 가장 가까운 스냅샷들인 A2, B2, C3만으로 특정 버전을 만들어낼 수 있다.

체크섬


Git은 항상 데이터를 저장하기 전에 체크섬을 구하고 그것으로 데이터를 관리한다.
그래서 체크섬을 이해하는 Git 없이는 어떤 파일이나 디렉토리도 변경할 수 없다.
체크섬은 Git에서 사용하는 가장 기본적인(Atomic) 데이터 단위이자 기본 철학이다.
심지어 체크섬을 다룰 수 있는 Git이 없다면 전송 과정에서 정보를 잃거나 파일 손상을 입을 수조차 없다.

24b9da6552252987aa493b52f8696cd6d3b00373
Git은 SHA-1 해시를 사용해 위와 같이 체크섬을 만든다. 이는 40자 길이의 16진수 문자열이며
파일 내용이나 디렉토리 구조를 이용해 구한다.

Git은 모든 것을 해시로 식별하기에 이런 값을 자주 볼 수 있다.
실제로 Git은 파일을 이름으로 저장하는게 아니라 해당 파일의 해시로 저장한다.

Modified, Staged, Committed


Git은 파일을 Modified, Staged, Committed세 가지 상태로 관리한다.
이 세 가지 상태는 각각 Working Tree, Staging Area, Git directory세 가지 단계와 연결되어 있다.

출처: git-scm의 pro git book Chapter 1.3

세 가지 상태

  • Modified: Working Tree에서 파일을 수정했으나 Stage하지 않은 상태.
  • Staged: Working Tree에서 수정한 파일을 Stage하여 곧 커밋할 것이라고 표시한 상태.
  • Committed: 데이터가 로컬 데이터베이스인 Git directory에 영구적인 스냅샷 형태로 안전하게 저장된 상태.

세 가지 단계

  • Working Tree: 프로젝트의 특정 버전을 checkout한 것.
  • Staging Area(i.e index): Git directory에 있는 단순한 파일로 곧 커밋할 파일에 대한 정보를 저장한다. Git에서 기술 용어로 index라고 하지만, Staging Area라고 불러도 상관 없다.
  • .git directory: Git의 핵심으로 프로젝트의 메타 데이터와 객체 데이터베이스를 저장하는 곳이다. 다른 컴퓨터의 저장소를 clone할 때 만들어진다.

HEAD


브랜치 혹은 특정 커밋을 가리키는 포인터.
정확하게는 현재 브랜치의 마지막 커밋의 스냅샷을 가리킨다.
이는 다음 커밋의 부모이기도 하다.

attached HEAD state

일반적으로 HEAD는 브랜치를 가리키고, 그 브랜치는 특정 커밋을 가리킨다.
이렇게 HEAD -> 브랜치 -> 특정 커밋의 순으로
HEAD가 커밋을 간접적으로 가리키는 상태를 attached HEAD state라 한다.

detached HEAD state

위와 반대로 HEAD가 커밋을 직접적으로 가리키는 상태를 detached HEAD state라 한다.
old commit이나 remote branch에 checkout하면 이 상태가 된다.

plumbing, porcelain

plumbing 명령어: 저수준 명령어
porcelain 명령어: 좀 더 사용자에게 친숙한 사용자용 명령어

branch(feat.Refs)


실제로 Git의 브랜치는 어떤 한 커밋을 가리키는 40글자의 SHA-1 체크섬 파일에 불과하다.
그 파일의 이름이 master라면 master 브랜치가 되고
develop이면 develop 브랜치가 되는 것이다.
이는 기억하기 힘든 SHA-1을 날 것 그대로 사용하기보다
기억하기 쉬운 파일 이름을 포인터로 활용하는 것이다.
Git에서는 이를 "References" 혹은 "Refs"라고 부른다.

어떤 커밋 1a410e 이전의 모든 히스토리를 보려면 git log 1a410e를 실행하면 가능하지만,
1a410e를 기억해야 한다.
기억하기 쉬운 이름을 포인터로 활용하면 편할 것이다.

아래 명령으로 40글자의 SHA-1 체크섬을 내용으로 하는 master라는 파일을 refs에 생성한다.

$ echo 1a410efbd13591db07496601ebc7a059dd55cfe9 > .git/refs/heads/master

그러면 이제 1a410e 없이 master라는 Refs만으로 히스토리를 조회할 수 있다.

$ git log --pretty=oneline master

1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

새로 브랜치를 하나 만드는 것은 41바이트 크기의 파일(40자와 줄 바꿈 문자)하나 만드는 것일 뿐이다.
브랜치를 새로 딸 때 프로젝트를 통째로 복사해야 하는 다른 버전 관리 도구와 Git의 차이는 여기서 갈린다.

그 뿐 아니라 커밋을 할 때마다 이전 커밋의 정보를 저장하기에 브랜치에서 작업 후 Merge할 때 어디서부터 합쳐야 하는지(Merge Base) 알고 있다.

아래 이미지처럼 브랜치는 어떤 커밋을 가리킬 뿐이고
첫 커밋(git init)을 제외한 모든 커밋은 이전 커밋에 대한 정보를 저장하기에 Merge Base를 알고 있으며
HEAD는 checkout한 브랜치를 가리킬 뿐이다.
HEAD -> 브랜치 -> 어떤 커밋 (일반적으로 해당 브랜치의 마지막 커밋)

출처: git-scm의 pro git book Chapter 3.1

tag


plumbing 레벨에서 보면 특정 커밋에 대한 포인터(ref)로 브랜치와 개념이 동일하다.
porcelain 레벨에서는 git branch, git tag의 명령어로 조작되어 사용성이 다르며
checkout, reset, fetch, push 등 기타 porcelain 명령어에 의해 영향을 받는 부분도 다르다.

브랜치는 기본적으로 브랜치가 자라면서 마지막 커밋을 계속 참조하도록 설계되었다.
fast-forward merge 케이스에서는 force 옵션 없이도 덮어써진다.
또한, 메타 데이터를 담을 수 없다.

태그는 기본적으로 한 커밋을 고정적으로 가리키도록 설계되었다.
어떤 경우에도 force 옵션 없이는 덮어쓸 수 없다.
또한, 메타 데이터를 넣을 수 있다. (annotated tag)

lightweight tag

특정 커밋에 대한 포인터이다.
파일에 커밋 체크섬을 저장하는 것뿐이다.
파일 이름이 곧 tag의 이름이다.
이름만 달아줄 뿐이다.

$ git tag v1.4

annotated tag

특정 커밋에 대한 포인터이자 메타 데이터를 담고 있다.
태그 작성자, 날짜, 태그 메시지, 서명 등을 저장할 수 있다.
git tag 명령에 -a 옵션을 주면 되며
-m 옵션으로 태그 메시지를 남길 수 있다.
명령 실행 시 태그 메시지를 남기지 않으면 Git이 편집기를 실행한다.

$ git tag -a v1.4 -m "my version 1.4"

git show 명령으로 태그와 커밋 정보를 모두 확인할 수 있다.

$ git show v1.4
tag v1.4
Tagger: Ben Straub <ben@straub.cc>
Date:   Sat May 3 20:19:12 2014 -0700

my version 1.4

commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Mon Mar 17 21:52:11 2008 -0700

    changed the version number

tag를 checkout하면 detached HEAD state가 된다. 여기서 바로 작업 후 커밋하면 새 커밋에 도달할 방법이 없게 되므로, 새로운 커밋이 버그 픽스와 같은 유의미한 작업으로 남게 하려면 새로운 branch로 checkout 후 작업하는게 좋다.
물론 그렇게 하더라도 tag는 새로운 커밋이 아니라 기존 커밋을 가리킨다.

또 git push 명령은 자동으로 tag를 remote repository에 전송하지 않는다.
tag를 만들었다면 별도로 push해야 한다.
아래와 같이 할 수 있다.

$ git push origin <태그이름>

--tags 옵션을 쓰면 remote repository에 없는 태그를 모조리 전송할 수 있다.

$ git push origin --tags

reset, checkout (경로 없음 기준)


일반적인 의미에서 Git이 세 가지 트리를 관리하는 시스템이라 생각하면 reset과 checkout을 이해하기 쉽다.
여기서 트리란 자료구조의 트리가 아닌 파일의 묶음을 의미하며,
세 가지 트리는 아래와 같다.

  • HEAD: 현재 브랜치의 마지막 커밋의 스냅샷(i.e 다음 커밋의 부모 커밋)
  • Index(i.e Staging Area): 다음에 커밋할 스냅샷
  • Working Directory: 샌드박스 (다음 커밋을 위한 작업 공간)

reset

--soft

아래는 master 브랜치에서 v1, v2, v3 세 번의 커밋 이후 git reset --soft 명령을 실행하기 전 상태이다.

출처: git-scm의 pro git book Chapter 7.7

아래는 git reset --soft HEAD~ 명령을 실행한 뒤의 상태이다.

출처: git-scm의 pro git book Chapter 7.7

위에서 알 수 있듯이 soft 옵션을 사용하면 HEAD가 가리키는 브랜치를 옮긴다.
정확하게는 해당 브랜치가 가리키는 커밋을 변경한다.
그에 따라 해당 브랜치를 가리키는 HEAD도 자연스레 변경되고 Index, Working Directory는 그대로 남아있다.
결과적으로 세 가지 트리 중 HEAD만 변경되고 Index, Working Directory는 그대로 남게 된다.

--mixed (기본값)

아래는 위와 같은 상황에서 soft 옵션 대신 mixed 옵션으로 명령을 실행한 결과다.
참고로 git reset에 아무런 옵션도 주지 않으면 mixed를 기본값으로 동작한다.

출처: git-scm의 pro git book Chapter 7.7

mixed 옵션에서는 세 가지 트리 중 HEAD, Index가 변경되고 Working Directory는 그대로 남는다.

--hard

hard 옵션으로 실행하면 Working Directory까지 모두 변경한다.

출처: git-scm의 pro git book Chapter 7.7

Git에는 데이터를 실제로 삭제하는 방법이 몇 개 없는데, reset --hard가 그 중 하나다.
매우 중요하고 위험할 수 있다.
위 예제에서는 Git이 v3의 커밋을 보관하고 있기에 reflog를 이용하면 복구가 가능하긴 하지만,
커밋한 적 없는 데이터는 복구할 방법이 없다.

checkout

HEAD 자체를 다른 브랜치로 옮긴다.
브랜치를 옮기면 워킹 디렉토리의 파일이 가장 마지막 시점에 작업했던 상태로 변경된다.
파일 변경 시 문제가 생기면 reset --hard와는 달리 브랜치 이동 명령을 수행하지 않는다.
local change가 있는데 문제 없이 checkout되면 그대로 커밋하면 되고,
아래와 같은 에러 메시지를 뱉으면 stash 후 reapply 하면 된다.
error: Your local changes to the following files would be overwritten ...

reset vs checkout

git checkout [branch] 명령은 git reset --hard [branch] 명령과 유사하게 [branch] 스냅샷을 기준으로 세 가지 트리를 조작한다. 하지만, 두 가지 차이점이 있다.

  1. checkout 명령은 reset --hard 명령과 달리 워킹 디렉토리를 안전하게 다룬다. 저장하지 않은 데이터가 있는지 확인해서 날려버리지 않게 보장해준다.
    내부적으로 워킹 디렉토리에서 merge를 시도해보고 변경하지 않은 파일만 업데이트한다.
    반면 reset --hard는 확인을 거치지 않고 단순히 모든 것을 바꿔버린다.

  2. reset은 HEAD가 가리키는 브랜치가 다른 커밋을 가리키도록 변경하지만
    checkout은 HEAD 자체를 다른 브랜치를 가리키도록 변경한다.


출처: git-scm의 pro git book Chapter 7.7

fetch


git fetch 명령은 remote repository로부터 local repository로 commit, file, refs와 같은 데이터를 다운로드 한다.
git merge와 함께 local repository의 상태를 remote repository와 같도록 업데이트 할 때 주로 사용되는 Git의 핵심 명령이다.
fetch + merge = pull로 봐도 무방하다.
fetch는 pull의 safe version이다.
fetch 후 log 명령으로 변경사항을 확인하여 문제가 없으면 merge하는 방식으로 안전하게 local repository를 업데이트 할 수 있다.

$ git fetch origin

위와 같은 명령은 아래와 같이 다운로드된 브랜치를 보여준다.

a1e8fb5..45e66a4 master -> origin/master
a1e8fb5..9e8ab1c develop -> origin/develop
* [new branch] some-feature -> origin/some-feature

그림으로 표시하면 아래와 같다.


출처: Atlassian Git tutorials - git fetch

local master branch에 변경점들을 확인하려면 아래와 같이 하면 된다.

$ git checkout master
$ git log origin/master

이상이 없다고 판단되어 merge하려면 아래와 같이 하면 된다.

$ git merge origin/master

merge


Git의 merge는 분기된 히스토리를 하나로 합치는 명령이다.
독립적인 라인에서 따로 개발되던 브랜치들을 하나의 브랜치로 통합할 수 있다.
merge의 방식은 두 가지가 있다.

Fast Forward merge

current branch가 target branch와 선형적(linear)인 관계일 때 가능하다.
즉, 둘은 분기된 적이 없어야 한다.
여기서는 실제로 두 브랜치를 merge한다기 보다는, current branch의 끝(마지막 커밋)을 target branch의 끝(마지막 커밋)으로 옮기기만 할 뿐이다.


출처: Atlassian Git tutorials - git merge

3-way merge

current branch와 target branch가 분기되어 개발이 진행되었을 경우 사용한다.
current branch의 마지막 커밋, target branch의 마지막 커밋, 둘의 공통 조상(merge base)커밋
이렇게 세 가지 커밋을 참조한다고 하여 3-way merge라고 이름 붙었다 한다.
3-way merge를 하면 current branch에 변경점들의 히스토리가 담긴 새로운 merge commit이 남는다.


출처: Atlassian Git tutorials - git merge

pull


pull은 간단히 말하면 fetch 후 merge한 상태다.
fetch + merge의 shorcut이라고 봐도 무방하다.
fetch 명령으로 remote repository에서 commit, file, refs 등의 데이터를 다운로드하여
merge 명령으로 local repository를 즉시 업데이트한다.
remote repository의 변경점이 매우 사소하거나 local repository와 충돌이 일어날 가능성이 적다면 pull도 좋지만
그렇지 않다면 fetch에 비해 충돌이 발생할 확률이 높아서 사용 시 주의해야 한다.

push


fetch나 pull과 같은 다운로드 커맨드와 반대로 local repository의 데이터를 remote repository로 업로드한다.
git remote 명령으로 설정된 remote repository에 변경점을 업데이트한다.
Git은 push 요청으로 fast-forward merge가 아닌 다른 것이 발생하면 중앙 저장소 히스토리의 덮어쓰기를 방지하기 위해 push 요청을 거절한다.
--force flag를 사용하면 중앙 저장소를 덮어쓸 수 있으나 이는 매우 조심해야 한다.
다른 동료가 이미 덮어쓰기 전의 중앙 저장소를 기준으로 작업하고 있을 수 있기 때문이다.

rebase


master에서 분기된 feature 브랜치에서 작업이 한창 진행중인데 master에 추가 커밋이 생겼을 경우를 가정하자.
merge를 사용해 master의 추가 커밋을 feature로 합쳐도 되지만, 그러면 merge commit이 남으니 히스토리가 지저분해지므로 나중에 디버깅을 할 때 방해 요소가 될 수 있다.
위와 같은 상황에서 merge 대신 rebase를 사용하면 feature 브랜치가 master의 추가 커밋에서부터 분기되어 출발한 것처럼 보이게끔 해주는 마법이다. 새 출발 시켜준다.

출처: Atlassian Git tutorials - git rebase

내부적으로는 완전히 새로운 커밋을 생성하므로, 공개 저장소에 push한 commit은 rebase하면 안된다.
동료 입장에서는 아래의 C4 커밋이 갑자기 사라진 것처럼 보일 것이다.
히스토리가 엉망이 되고 욕을 오지게 먹을 수 있다.

출처: git-scm의 pro git book Chapter 3.6

rebase는 위 주의사항만 조심하며 잘 사용하면 클-린한 히스토리와 딱 필요한 커밋으로 새로운 기능을 개발하는 멋진 개발자처럼 보이게 해줄 수 있는 멋진 기능이다.

cherry-pick


특정 커밋을 골라 checkout한 브랜치에 바로 적용할 수 있는 유용한 명령으로 아래와 같이 실행한다.

$ git cherry-pick commitSha

아래와 같은 상황이라고 가정하자.

 a - b - c - d   Master
         \
           e - f - g Feature

f 커밋을 Master 브랜치에 집어넣고 싶다면 아래와 같이 하면 된다.

$ git checkout master
$ git cherry-pick f

// result
a - b - c - d - f   Master
         \
           e - f - g Feature

cherry-pick은 아래와 같은 상황에서 유용하다

  • 하나의 프로젝트에서 프론트엔드와 백엔드가 협업 시 공유해야 할 코드가 있을 때.
  • 엔드 유저에게 빠르게 전달되어야 할 수정 사항을 적용할 때.
  • QA중인 release 브랜치에 뒤늦게 개발된 간단한 수정사항을 적용할 때.

🔎 참조


profile
Maker를 지향하는 웹 개발자입니다.

0개의 댓글