팀 프로젝트를 진행할 때, Git에 대한 이해가 부족해서 Git을 잘 알고 있는 팀원분의 의견에 그대로 따라했던 기억이 난다.
GitHub 브랜치 전략을 사용하고 squash merge를 사용하고 커밋 컨벤션을 사용하고 등등..
그 당시에는 자세하게 공부할 시간이 부족해서 따라하는 데에 급급했지만, 이제는 정리하고 이해할 시간이 된 것 같다.
다음에 또 Git을 집중적으로 사용할 일이 있을 때, 어떤 전략을 사용하는 것이 올바른지 이해하기 위해서이다.
모든 버전 관리 시스템은 브랜치를 지원한다. 개발을 하다 보면 코드를 여러 개로 복사해야 하는 일이 자주 생긴다. 코드를 통째로 복사하고 나서 원래 코드와는 상관없이 독립적으로 개발을 진행할 수 있는데, 이렇게 독립적으로 개발하는 것이 브랜치다.
Git에서는 브랜치를 위와 같이 설명하고 있다.
즉, 자신의 코드를 여러 버전으로 분리하여 독립적으로 관리하고 싶을 때 사용할 수 있는 개념이 브랜치라는 것이다.
컴퓨터 용어 중 스냅샷
이라는 단어를 접한 적이 있을 것이다.
스냅샷은 특정 시간(시점)에 데이터 저장 장치(스토리지)의 파일 시스템을 포착해 별도의 파일이나 이미지로 저장, 보관하는 기술을 말한다.
Git에서도 이런 스냅샷이라는 개념을 이용하여 소스코드의 상태를 관리해 준다.
커밋
이란 이러한 스냅샷을 만들어내는 작업을 말한다.
커밋이 발생해야만 Git이 해당 시점의 코드를 기억할 수 있기 때문에, 커밋을 단위로 이전 히스토리를 확인하거나 롤백이 가능해 진다.
하나의 브랜치에서 다른 브랜치간에 변경사항을 합쳐 새로운 커밋을 만드는 과정
쉽게 생각해서 브랜치를 합쳐서 새로운 커밋으로 만드는 것을 말한다.
브랜치를 통해 코드를 독립적인 버전으로 관리한다는 말은 완전히 다른 길로 나아가는 코드가 될 수도 있지만, 기존의 브랜치에서 추가되거나 고쳐야 할 내용들을 새로운 브랜치에서 관리할 수 있다는 의미이기도 하다.
그런 경우 관리하던 브랜치를 기존의 브랜치와 합쳐야 하는 경우가 발생하고 그럴 때에 머지를 사용한다.
Fast-Forward
방식은 브랜치로 분기되던 시점의 커밋(이를 Base
라고 한다.) 이후에 기존 브랜치에 커밋이 없는 경우에 발생하는 Merge
이다.
위의 그림처럼 브랜치2는 브랜치 1의 B커밋(Base)에서 분기되었지만 브랜치1은 B를 기점으로 더 이상의 커밋이 없다.
이 상태에서 브랜치2의 작업 내역을 브랜치1로 머지하는 경우
이렇게 C와 D커밋이 B를 기점으로 다시 이어지기만 하면 되는 형태이다.
분기된 브랜치가 base
에 이어지는 개념이므로 충돌이 발생할 일 없이 그대로 가져올 수 있고 커밋 히스토리 또한 이어진다.
이렇게 특별한 merge
과정 없이 최신 커밋에 이어지며 그대로 이동하는 방식을 Fast-Forward Merge
라고 한다.
Recursive
방식은 Fast-Forward
와 다르게 기존 브랜치에서 커밋이 더 발생한 경우이다.
위 처럼 두 브랜치가 같은 Base
로부터 커밋된 이력이 있기 때문에 브랜치2가 브랜치1과 머지되기에는 커밋 이력들이 있어서 충돌 가능성이 있다.
이런 경우에는 3-way merge
방식을 사용하여 커밋 이력을 생성하는데, 이러한 머지 방식을 Recursive Merge
라고 한다.
3-way merge란?
기존의 브랜치들과 Base
를 비교하여 merge
하는 방식을 말한다.
A와 B와 C라는 파일이 존재하는 코드가 있다고 가정해 보자.
위에서 B커밋(Base
)을 기준으로 각 브랜치에서 변경된 작업과 비교하면 어떤 장점이 있는지 그림으로 알아 보자.
브랜치1과 2 모두 A에는 변경이 없기 때문에 Merge할 때 A를 유지할 수 있다.
B는 브랜치2만 변경했으므로 브랜치2의 B로 변경된다.
C는 두 브랜치에서 모두 변경했으므로 충돌이 발생한다.
이렇게 두 브랜치에서 변경된 기준점을 Base
로부터 비교하기 때문에 Merge할 때에 충돌되는 이력들을 확인할 수 있다.
만약 Base
가 없이 두 브랜치만 비교한다면, 변경이 일어났는지 아닌지를 판가름할 기준이 없어, 모든 파일을 하나하나 비교하여 변경점이 있는지 확인해야 하는 문제가 생기지 않을까 싶다.
그런 점에서 본다면 3-way merge
는 상당히 효율적인 방식이다.
Recursive merge
가 수행되고 나면 위처럼 브랜치2에서 작업했던 내역이 커밋 이력으로 남게 된다.
rebase
도 merge
와 마찬가지로 두 브랜치를 하나로 합치는 방법 중 하나이다.
다만 merge
와는 조금 다르게 rebase
는 기존의 base
를 다시 조정하는 개념으로 이해하면 된다.
위와 같은 두 브랜치가 있을 때, 브랜치2의 base
는 B
이지만 이것을 D
로 옮기는 작업이라고 보면 된다.
이는 마치 처음부터 base
가 D
였던 브랜치가 Fast-Foward merge
된 것과 같다.
즉, rebase
를 사용하면 Fast-Foward merge
를 할 수 있다는 뜻이다.
Fast-Foward merge
는 커밋 히스토리가 깔끔하게 이어지기 때문에 필요에 따라 merge
대신 사용할 수 있다.
또한 rebase
는 그 외에도 --onto
옵션을 활용하면 특정 상황에서 도움이 될 수 있다고 한다. [링크 참조]
Squash & Merge
는 히스토리 관점에서 rebase
와 merge
를 합쳐 놓은 방식과 비슷하다.
브랜치에서 발생한 커밋 이력을 통합하여 하나의 커밋 이력을 만들어 내기 때문이다.
기존의 merge
방식과 유사하면서도 커밋 이력이 사라진다는 점에서 히스토리는 마치 rebase
와 비슷한 형태를 갖는다.
위 그림처럼 브랜치2에서 발생했던 커밋 이력들은 하나로 합쳐져 G커밋이 되었다.
히스토리가 사라지기 때문에 깔끔한 히스토리 관리가 가능해진다.
이러한 장점 때문에 불필요하게 히스토리가 지저분해지거나 하는 경우 변경된 이력에만 집중해야 하는 경우 유용하게 사용될 수 있다.
그리고 새로 생성된 커밋 이력 안에는 브랜치에서 작업했던 커밋 이력도 확인이 가능하기 때문에 기존 히스토리가 완전히 사라지는 것은 아니다.
다만 상세한 이력 확인을 위해서는 결국 통합된 커밋 이력을 통해야 하기 때문에 추적이 쉽지는 않다는 단점이 있다.
revert
는 커밋 이력을 되돌리는 것을 말한다.
Git을 사용하다 보면 Commit을 잘못하는 경우가 발생하여 되돌리고 싶은 상황이 발생한다.
이럴 때 사용할 수 있는 방법이 revert
와 reset
이 있는데, revert
는 이전 내역을 되돌리면서 커밋을 만들어내는 방식이다.
자신이 커밋한 이력이 잘못되어 이전 이력으로 되돌아간다는 것을 커밋으로 만들어 내어 이전 커밋 이력으로 코드를 되돌리고 커밋을 새로 생성하는것이다.
예를 들어, A -> B -> C 커밋이 있는데 C가 잘못된 커밋이어서 B로 되돌아가려 한다고 가정해 보자.
이 때 revert
를 사용하면 A -> B -> C -> B 이런 형태로 커밋을 만들면서 이력을 되돌린다는 뜻이다.
reset
은 커밋을 완전히 없었던 일로 만들어 버리는 개념이다.
커밋이 일어나기 전의 상황으로 시간을 되돌린다고 생각할 수 있다.
위 처럼 A -> B -> C 커밋이 있고 B로 되돌아가려 한다면, reset
을 이용해 A -> B로 커밋 이력을 만들어 버릴 수 있다.
reset
을 사용하면 C로는 다시 돌아갈 수 없는가?
그런 아니다. Git은 커밋, 체크아웃, 머지, 리셋 등의 이력을 모두 기록하고 있기 때문이다.
reset
을 사용하더라도 Git이 기록한 로그를 통해 다시 reset
내용을 취소할 수 있다.
자세한 취소 방법은 필요한 경우 찾아 보도록 하고, 가능하다는 것만 알아 두자.
push
는 로컬 브랜치의 이력을 원격 저장소에 반영하는 것을 말한다.
로컬의 작업은 다른 사람들과 공유되지 않기 때문에 협업을 할 때에는 원격 저장소를 통해 코드가 관리된다.
그렇기 때문에 다른 사람에게 내 현재 작업사항을 반영시켜 주기 위해서는 원격 저장소에 내 브랜치를 push해야 한다.
push
를 할 때에는 충돌이 발생하면 안 되기 때문에 항상 로컬 브랜치의 상태를 원격 저장소의 최신 상태와 맞도록 유지해야 한다.
( 강제로 push
하는 방법도 존재한다. )
pull
은 push
와 반대로 원격 저장소에 반영된 이력을 나의 로컬 브랜치로 가져오는 것을 말한다.
pull
은 원격 저장소의 이력을 가져오면서 자동으로 merge
하는 기능을 포함하고 있다.
따라서 pull
을 사용하고 나면, 로컬 브랜치는 원격 저장소와 같은 상태가 된다.
push
를 하기 이전에 항상 pull
을 해 주는 이유가 이 때문이다.
fetch
또한 pull
과 마찬가지로 원격 저장소에 반영된(변경된) 이력을 나의 로컬 브랜치로 가져오는 것을 말한다.
하지만 pull
과 다르게 merge
를 하지는 않는다.
단순히 원격 저장소의 이력만을 다운로드하여 최신 상태로 반영하는 것이라고 생각하면 된다.
fetch
를 통해 이력을 가져온 뒤 merge
하면 pull
을 사용한 것과 같다.
신중하게 이력 확인이 필요하다면 fetch를 사용하면 좋을 듯 하다.
clone
은 원격 Repository
를 로컬로 연결하고 복사해 오는 것을 말한다.
말 그대로 원격 리포지토리의 모든 소스를 로컬로 복사하고 연결하여 작업할 수 있도록 하는 기능이다.
fork
는 Repository
자체를 복사하는 것을 말한다.
원본 리포지토리를 나의 리포지토리로 복사하여 가져온다는 뜻이다.
복사해 온 리포지토리를 clone으로 로컬과 연결하여 나만의 작업을 할 수도 있고, 해당 작업들이 원본 리포지토리에 반영되길 바란다면, 나의 변경 사항을 원본 리포지토리에 Pull Request로 요청할 수도 있다.
이제 Git의 기본적인 용어들이 정리가 되었으니 팀프로젝트에서 사용했던 브랜치 전략이나 merge방식에 대해 정리해 봐야겠다.