Using git like a pro

Git 100% 활용하기: 협업을 위한 브랜치 전략, 팁과 노하우

2016년 360|AnDev에서 git의 고급 활용법을 주제로 발표한 강연입니다. 팀 구조에 맞게 빠른 워크플로를 달성하기 위해 활용할 수 있는 다양한 브랜치 전략과 Shazam 안드로이드 팀에서의 브랜치 활용 전략 시연, 작업 방식을 최적화하고 다른 사람들과 공유할 수 있는 방법을 배울 수 있습니다.


저는 Savvas Dalkitsis 이며, Shazam의 수석 소프트웨어 엔지니어로 일하다 ASOS.com의 리드 안드로이드 개발자로 이직했습니다. Twitter 계정은 @geeky_android를 사용합니다.

소개 (0:30)

이 강연은 버전 컨트롤, 혹은 형상 관리에 대해 다루므로 당연히 git 에 초점을 맞추고 있습니다. SVN, Mercurial, Perforce 등 다른 소스 컨트롤 시스템도 많은데요. 많은 개발자들은 형상 관리를 사용하고, 대부분이 git을 사용하죠. 왜 형상 관리를 해야 할까요?

형상 관리를 해야 하는 이유 (1:00)

여기 제가 어렸을 때 사용하던 486 컴퓨터에서 분해한 하드 드라이브 사진이 있습니다. 여기서 제가 열 살 무렵에 작성했던 엉망진창인 옛 코드를 찾았는데요. 문제는 코드의 조악성이 아닙니다.

제 소스를 위해 엄청나게 큰 폴더를 사용하고 있었는데, 이 중에는 컴파일되지 않은 상태인 것도 있습니다. 어떤 생각을 하고 있었는지 알아보기 위해 다시 컴파일하려고 했지만 실패했죠. 코드를 몇 줄 적고 버려두고는 다시 컴파일하지 않았던 겁니다. 더욱이 어린 시절에 어떤 과정을 거쳐 소프트웨어를 개발했는지 파악할 수도 없었습니다. 결국 그 소프트웨어를 작성하면서 어떤 과정에서 개발이 멈췄는지 다시 파악할 수는 없었습니다.

형상 관리를 사용하는 다른 장점은 개인으로건, 팀으로건 작업할 때입니다. 처음에는 전체 프로젝트의 이미지를 머릿 속에 그려둘테지만 시간이 흐르면 이런 이미지가 희미해질 수 있습니다. 특히 팀과 함께 일한다면, 같은 소프트웨어를 같이 개발하는 사람들 사이의 질서는 필수적으로 지켜야 합니다. 또한 좋은 의미로 책임 소재를 명확히 하는데도 도움이 되죠. 특히 버그를 발견해서 고쳐야할 경우 쉽게 그 코드를 작업한 사람이 누군지 파악해서 도움을 요청할 수 있습니다. 형상 관리가 없다면 이런 일은 불가능하겠죠.

작업이 진행될 수록 다른 문제들이 속속 발생합니다. Shazam의 글로벌 오피스는 총 7개였는데 이 중 두 세개는 엔지니어링 팀을 내부에 갖고 있었고, 이들 오피스 사이에서 코드가 공유됐죠. 이전에는 대부분의 팀이 독립적이었지만, 한 팀원이 미국으로 이주한 이후 정말 유용한 분산 형상 관리 시스템을 구축할 수 있었습니다. 앞서 말한 책임 소재 등 많은 문제가 해결됐을뿐만 아니라 여러 유형의 워크플로도 가능해졌습니다.

GitHub, Stack OverFlow, Linux kernel 등 많은 대형 조직에서는 전세계에 흩어진 여러 팀에서 코드가 개발되고 있다는 걸 들어보셨을 겁니다. 이들 조직이 주기적으로 몇 달에 한 번씩 병합 과정을 갖는다고 생각해보죠. git을 사용한다면 다른 팀과 함께 배포할 수 있는 일종의 반쯤 분리된 저장소를 가질 수 있습니다.

형상 관리가 이런 많은 문제를 해결하는데 어떻게 도움이 되는 걸까요? git에 대해 간략하게 살펴보고 나서 Shazam에서 사용했던 방법을 말씀드리겠습니다. 이미 git을 사용하고 있거나 잘 알고 있는 분에게도 유용한 얘기일텐데요, 왜냐면 문제 해결에 도움이 되는 방식을 이해하려면 어떻게 작동하는지 실제로 알아야 하기 때문입니다. git 전문가라고 자부할 수 없거나, commit이 무엇이고 다른 것과 어떻게 관련이 되는지와 같은 git의 내부 작동 방식을 잘 알지 못하는 분이라면 주목해 보세요.

git과 SVN의 차이점 (4:16)

먼저 git과 다른 유명한 제어 엔티티를 비교해 볼까요? SVN은 git 이전에 유명했던 형상 관리 시스템이지만 git과는 작동 방식이 다릅니다. git의 장점에 초점을 맞춰 이 둘을 비교해보겠습니다. 좌측이 git, 우측이 SVN입니다.

using-git-like-a-pro git-vs-svn-01

SVN과 비교되는 git의 대표적인 차이점은 git이 분산식이라는 점입니다. 반면 SVN은 중앙집중식이죠. SVN에서는 서버 어딘가에 중앙 저장소가 있고, 해당 저장소에서 코드를 사용해야 할 때 로컬에서 해당 저장소의 버전을 체크아웃해야 합니다. 따라서 두명의 사용자가 저장소의 동일 버전을 체크아웃하려면 전체 저장소를 체크아웃해야 하고, 사용자의 컴퓨터에 저장되는 것은 실제 저장소가 아니라 원본을 복제한 미러 버전입니다. 작업을 수행하려면 저장소와 지속적으로 통신해야만 하죠.

하지만 git에는 중앙 저장소가 없습니다. 절대적인 소스를 가진 중앙 저장소로 사용할 수는 있지만 필수 요소는 아닙니다. 저장소를 체크아웃하는 사용자는 작업 중인 저장소 전체를 복제하며, 이를 실제로 다른 사람들이 중앙 저장소로 사용할 수도 있습니다.

물론 상기 예처럼 중앙 저장소가 있고 모든 사용자들이 같은 곳에 커밋하는 SVN과 유사한 방식으로 git을 사용할 수는 있습니다. 또한 2번 저장소가 4번 저장소에 직접적으로 통신하거나, 모든 저장소가 각각의 저장소와 통신하는 이상한 구조를 만들고 하나의 절대 저장소에 업데이트할 수도 있겠지만 이를 권장하지는 않습니다.

이런 개발 뉴스를 더 만나보세요

SVN에는 없는 git의 또다른 장점은 오프라인 작업이 가능하다는 점입니다. 즉, SVN에서는 코드를 푸시(SVN 용어로는 커밋)할 때 온라인 연결이 필수적입니다. 로컬에서 변화가 있으면 중앙 저장소에 커밋해서 다른 사람들이 받을 수 있도록 해야 하죠. 오프라인일 경우에는 불가능한 일이므로 커밋할 수가 없습니다. 변화 내용은 온라인이 되기 전까지 그저 컴퓨터에 남아있을 뿐입니다.

이런 경우 네트워크 연결이 안될 때 작업을 유연하게 할 수 없으므로 많은 문제점이 발생할 수 있습니다. 단지 비행기 탑승이나 컨퍼런스 연설 때와 같은 특별한 경우에만 문제가 되는건 아닙니다. 사무실에 있는 경우에도 모든 사람들이 사용하는 메인 라우터가 고장나는 등의 IT 문제가 발생한다면 팀 모두가 커밋을 푸시할 수 없어서 작업을 멈춰야만 하죠.

git을 사용하면 로컬에서 커밋을 대기열에 넣고, 커밋을 푸시할 수 있습니다. 푸시라는 용어는 다른 소스 제어 시스템에서도 비슷하게 사용하지만 정확하게는 방식이 다릅니다. 처음 git이 인기를 모으던 시점에서는 많은 사람들이 커밋푸시 라는 용어 때문에 혼란스러워 했죠. 커밋은 로컬 작업이며 네트워크가 연결된 경우 푸시를 할 수 있습니다. 이 방식으로 중앙 저장소에 접근하지 않고도 작업을 이어갈 수 있습니다.

using-git-like-a-pro git-vs-svn-03

많이 알려지지 않은 다른 차이점은 git의 논리적 단위가 커밋 이라는 사실입니다. 반면 SVN의 논리적 단위는 개정판(revision) 이죠.

즉, SVN에서 특정 시점의 저장소 전체의 스냅샷 이 논리적 단위라는 뜻입니다. SVN은 이런 스냅샷을 바탕으로 작업하므로 새 개정판이 있을 때마다 실제로 전체 트리를 복제하지 않습니다. 대신 각 개정판을 전체 시스템의 한 스냅샷으로 처리합니다. 매번 커밋을 할 때마다 SVN은 논리적으로 전체 스냅샷을 하나의 개정판으로 취급하는데, 잠시 후 이 방식의 문제점에 대해 말씀드리겠습니다.

using-git-like-a-pro git-vs-svn-04

git에서의 논리적 단위는 커밋으로 독립적인 변화의 묶음입니다. 또한 커밋은 단순히 커밋한 코드뿐만 아니라 관련된 다른 데이터도 포함합니다. 변화가 일어날 때마다 이전 커밋과의 변경점을 추적합니다. 스냅샷이란 개념은 없습니다. 다시 말해, 전체 소스에 대한 스냅샷은 git에 없습니다.

git의 기술적인 차별성은 여기에서 나옵니다. SVN에서는 저장소에 사용자 저장소 의 실제 파일을 저장합니다. 개정판을 체크아웃하면 전체 소스가 컴퓨터에서 파일 시스템으로 체크아웃됩니다. 따라서 브랜치에 대한 폴더가 따로 생기고, tag에 따른 폴더로 분리되며, 각각 내부에 각 커밋에 대한 메타데이터를 담고 있는 .svn이 생성됩니다.

git은 조금 다르게 동작하는데요. 전체 저장소는 사용자 컴퓨터의 .git 폴더 안에 다운로드되고, 이 폴더 외부는 사용자의 작업공간이 되는데, 꼭 전체 저장소일 필요는 없습니다. 특정 브랜치가 있다면 해당 브랜치의 코드를 시스템에 체크아웃할 수 있죠.

다른 주요 차이점은 브랜치 입니다. SVN의 브랜치는 폴더이고, git은 단순한 커밋이죠.

SVN에서 새 기능 브랜치를 만드려면 말 그대로 폴더를 만들면 됩니다. 두 개의 기능 브랜치가 있다면 파일 시스템에 독립된 폴더가 있어야 하죠. 매번 브랜치를 만들 때마다 새로운 폴더가 생성됩니다.

git의 경우 브랜치는 이름이 붙은 커밋 입니다. 커밋은 다음 같이 몇 가지 중요한 부분으로 구성됩니다.

  • 코드 diff는 이전 커밋과 달라진 것을 뜻하는데, 사실 이전 커밋이 실재하지는 않습니다. 잠시 후에 더 말씀드리죠.
  • 메타데이터 집합: 작성자의 이름, 커밋 시점 등
  • 이전 커밋을 가리키는 해시 코드. 브랜치 히스토리를 git이 판별하는 중요한 역할입니다.

이런 정보들이 모두 모여 커밋을 형성하며, 이런 정보가 조금이라도 변경되면 다른 커밋이 됩니다. 따라서 코드 diff가 동일하더라도 작성자가 바뀌거나 시간이 바뀌거나 이전 해시가 바뀐다면 다른 커밋이 되는 겁니다.

이 모든 것이 계산돼서 해당 커밋을 식별할 수 있는 암호학적으로 안전한 해시를 만듭니다. 따라서 이런 정보가 변경되면 커밋 해시 역시 변경됩니다. 이들 모두는 기본적으로 원으로 표시되며 보통은 git 히스토리를 저장하는 트리에 연결됩니다.

앞서 말한 이전 해시는 기본적으로 이 전체 커밋의 해시 코드입니다. 이 방식으로 git은 히스토리를 만들죠. 폴더나 브랜치의 개념이 따로 없고, git의 브랜치는 특별할 것 없이 이들 해시에 이름을 붙인 것 뿐입니다. 즉, a4bgf3기능_1이라고 부르는 셈이죠. 해시 코드에 붙인 라벨일 뿐이며 다른 점이나 특별한 점은 전혀 없습니다. master 역시 마찬가지이므로 master 브랜치라고 git에 특별한 값을 가지고 있지는 않습니다. 반면 SVN에서의 trunk 는 저장소의 루트이죠.

다른 점이 또 있습니다. SVN은 히스토리를 보존하는 반면, git은 특정 규칙을 설정하지 않는 이상 변경할 수 있다는 점입니다.

SVN은 히스토리를 순차적으로 증가하는 개정판의 번호로 저장하므로, 어디에 코드를 푸시하건 전역 번호가 하나 늘어나 변경됩니다. 여러분이 브랜치에 푸시를 하고 제가 trunk가 아닌 마스터에 푸시한다면, 여러분의 커밋은 하나 증가하고 제 커밋 역시 하나 증가하죠. 따라서 여러분이 이전 커밋을 삭제하고 싶을 경우에는 이전 커밋을 되돌리는 새로운 커밋을 만들 수밖에 없으며, 여전히 히스토리는 유지됩니다. 이 경우 3번 커밋이 한 작업을 5번 커밋이 변경한 이력을 모두 볼 수 있죠.

같은 작업을 git에서도 할 수 있는데 다른 커밋을 되돌리는 새 커밋을 만들 수도 있겠지만, 해당 커밋이 필요없거나 뭔가 중요한 정보를 포함하고 있어서 없애고 싶은 경우라면 보다 나은 방식으로 작업할 수도 있습니다. 히스토리에서 커밋을 지워버리면 해당 커밋의 다음 커밋은 그 이전 커밋과 연결되며, 이전 커밋을 가리키던 숫자도 변경됩니다. 따라서 커밋은 다른 커밋이 되고, 새 해시 숫자를 갖게 되죠.

이 때문에 diff가 동일한 경우에도 커밋이 변경되며, 이 시점에서 이전 트리에 커밋을 적용하려는 충돌 사용이 있는 경우 실제로 여기에서 최종 커밋을 만들게 됩니다.

리모트에서 작업하기 (13:38)

using-git-like-a-pro remote01

이런 이슈들을 처리할 git 사용법을 이해하는 마지막 단계는 리모트 의 작동 원리를 이해하는 것입니다. 앞서 리모트 저장소와 로컬 저장소에 대해 이들 간의 차이점은 없다고 했었죠.

실제로 리모트 저장소를 체크아웃할 때를 생각해 볼까요? 해당 저장소에는 몇몇 라벨이 붙어 있습니다. 로컬에도 master가 있으며 origin/master 라는 것도 보입니다. “Origin”이란 리모트 저장소를 식별하려고 붙인 이름으로, 해시를 가리키는 라벨입니다. 따라서 masterorigin/master는 실제 저장소 마스터와는 다를 수 있습니다. 충돌 해결 시에 이들이 어떤 역할을 하는지 보여드리겠습니다.

using-git-like-a-pro remote02

리모트와 로컬에 동일한 저장소를 가지고 있는데 로컬 커밋을 하는 경우를 예로 들어 보겠습니다. master가 이동해서 새 라벨을 가리키며, origin/master는 이전 것을 가리킵니다. 변화 내용을 리모트 저장소에 푸시하면 간단하게 모든 것이 잘 작동합니다.

하지만 그 와중에 다른 사람이 리모트 저장소에 코드를 푸시하고 새 커밋을 만들었다면 origin/master는 실제 원격 master를 가리키지 않으므로 이를 해결해야 합니다. 주로 merge 를 하게 되는데, 이는 원격으로 작성된 새 커밋과 실제로 아직 올라가지 않은 로컬 커밋을 병합하는 것입니다. 이 때 트리는 다음 같은 모양이 되죠.

보시다시피 왼쪽 그림의 오른쪽 가지가 원격 저장소 구조를 반영하는 모습니다. 로컬 커밋과 리모트 간의 변경 사항을 병합하는 새 커밋이 생성됩니다. 사실 머지 문제 해결을 제외하고는 특정 목적을 수행하지 않는 커밋이니만큼 저는 이런 모양이 좀 깔끔하지 않다고 생각합니다.

하지만 정말 흔하게 발생하곤 하는 패턴이므로 많은 사람들이 rebase 로 이 문제를 해결하고 있습니다. git이 히스토리를 재작성하고 다른 작업을 할 수 있도록 지원해서 가능한 일입니다.

리베이싱을 하면 커밋을 잠시 버리고 현재 트리를 변경한 다음 커밋을 트리의 맨 위로 다시 적용할 수 있습니다. 리베이스를 해서 실제로 리모트로 올라가지 않은 이 커밋을 잠시 제쳐두고 실제 리모트의 커밋을 가져와 볼까요? origin/master는 이제 실제 리모트 master를 가리키게 되며, 새 origin/master의 제일 위에 커밋을 재적용할 수 있습니다. 물론 링크가 변경됐으니 누차 말씀드린대로 다른 커밋으로 변하겠죠.

이제 푸시해도 괜찮고, 세번째 커밋이 만들어지지도 않습니다. 히스토리에는 하나의 라인만 만들어졌고, 모든 것이 안정되고 보기 좋은 상태입니다. 어떤 일이 벌어졌는지 한눈에 파악할 수 있죠.

git이 관리해주고 있으므로 예전 커밋을 버려둘 수 있고 git에서 만들어진 모든 커밋은 항상 보존되긴 하지만, 라벨로 참조되지 않는 이상 트리에서는 보이지 않습니다. 따라서 git 트리를 볼 수 있는 보조 툴을 사용한다면 실제로 보이던 커밋이 전체 트리의 전부가 아님을 알 수 있습니다. 눈에 보이지는 않지만 나중에 복구할 수 있는 커밋들이 존재하죠. 또한 이런 커밋이 필요없다면 지울 수도 있습니다. 라벨이 없는 커밋은 해시로 참조할 수 있습니다.

Shazam의 브랜치 사용 전략 (17:05)

Shazam에서 git을 사용하는 방법을 한 문장으로 표현하면 이렇습니다. 항상 마스터에 푸시하고 배포를 위해 잠깐 중지하려면 브랜치를 사용한다.

using-git-like-a-pro shazam git tree

이게 무슨 뜻이나고요? 설명드리기 전에 저희 git 트리가 어떤 모양인지 잠시 보여드리겠습니다. 순차적인 저희 트리는 커밋에 하나의 라인만이 존재하므로 이해하기 아주 쉽습니다. 또한 배포할 때마다 저희가 온고잉이라고 명명한 해당 브랜치를 중지하고 배포 후보로 태그합니다. 해당 배포에서 버그를 발견하면 해당 브랜치를 핫픽스하고 마스터로 다시 머지해서 마스터가 최신 버전을 유지하도록 합니다. 배포가 완성될 때까지 이 과정을 반복하다보면 최종 배포 후보를 완성하게 되고, 마켓 배포로 실제 푸시할 수 있습니다.

이런 작업을 통해 저희는 두 가지 병렬 작업 모드를 사용할 수 있습니다. 팀 한쪽에서 다음 배포를 위한 기능 개발에 집중하는 동안, 다른 쪽에서는 실제 배포할 후보 코드를 견고하게 만들 수 있습니다. 다른 많은 전략을 사용해서도 이런 효과를 낼 수 있지만, 저희 전략의 주된 장점은 커밋의 히스토리를 아주 명료하게 볼 수 있다는 것입니다. 정말 단순하고 직선적인 선이므로 복잡할 것이 전혀 없습니다. 어떤 브랜치가 어디서 왔는지 파악하느라 머릿 속에서 씨름할 필요가 없죠. 어떤 브랜치에 각 커밋이 있는지 없는지 파악하려고 특별한 명령을 사용할 필요도 없습니다. 단순히 하나의 브랜치만 있으니까요.

단, 이런 작업 모드를 바로 도입할 수는 없고 몇 가지 전제 조건이 있습니다. 행위 주도 개발(Behavior-Driven Development, BDD)과 테스트 주도 개발(Test-Driven Development, TDD) 중심의 태도를 가져야 합니다. 마스터에 있는 각각의 커밋이 모든 것을 망가뜨릴 위험이 있기 때문이죠. 항상 배포에 준비돼있어야 하니 이런 상황을 바라지는 않으실 겁니다. 사장님이 불시에 들이닥쳐서 “자, 여태껏 개발한걸 오늘 바로 배포합시다.” 라고 선언하는 상황에 대비가 돼있어야 합니다. BDD, TDD를 사용하면 프로젝트가 항상 배포가 가능한 상태로 유지됩니다. 온전히 푸시할 수 있는 상태가 아닐 수도 있지만, 한 두 커밋 정도로 배포 후보를 만들에서 Play Store에 올릴 수 있어야 합니다.

핵심은 전체 BDD와 TDD 테스트 묶음을 매번 커밋할 때마다 실행하는 지속적인 통합 이 필요하다는 점입니다. 아쉽게도 Android 테스트는 느린 걸로 악명이 높기 때문에 Shazam에서도 이 작업을 수행하는 방법에 대해 많은 논의가 있어 왔으며, 지금은 속도를 높이기 위한 도구를 사용합니다. 또한 사람들이 빌드를 망치는 코드를 푸시하는 것을 막일 수 있는 사전 커밋 훅(pre-commit hook)을 사용합니다. 이에 따라 마스터에 푸시하는 순간에 코드가 git 저장소로 올라가지 않고, 단지 로컬 브랜치만이 생성되며, 리모트로의 통합은 Jenkins 통합 계획에 따라 테스트 묶음을 거친 후 실행됩니다. 테스트가 통과해서 녹색이 돼야만 실제 마스터로 올라가고 다른 사람들이 사용할 수 있는 상태가 되죠.

이 과정의 핵심은 항상 코드를 리베이스하는 태도입니다. 즉, 아침에 회사에 와서 첫번째로 하는 일이 커밋되지 않은 변화가 현재 마스터 위에 있다면 리베이스하는 것이어야 합니다. 리베이스를 사용하면 충돌도 쉽게 해결할 수 있습니다. 만약 두 개의 기능 브랜치가 있고 개발이나 마스터 브랜치와 항상 개발 상황을 맞추고자 하는 상황에서 이제 기반 브랜치를 사용하려 한다고 가정해 봅시다. feature 브랜치가 한참 진행된 후에 머지를 실행할 때, “충돌이 있을리가 없지, 매일 해결했는걸.” 이라고 생각할지도 모릅니다. 하지만 브랜치를 개발이나 마스터 브랜치와 통합하려고 하는 순간 다른 기능 브랜치가 머지를 했을 경우라면, 두 브랜치 사이에 이상한 충돌이 발생합니다. 이제 익히 겪어온 것처럼 해당 브랜치를 실제 작업한 사람에게 가서 충돌 해결 작업을 해야만 하죠.

모두가 마스터에 작업한다면 이런 변경 사항은 항상 한 두 커밋 정도에 발생하므로 고치기 쉽고 지엽적입니다. 갑자기 “내일 당장 배포해야 하니 두 기능 브랜치를 배포에 포함합시다.” 하는 통보에 놀라거나, “머지 과정은 최악이야, 도무지 고칠 수가 없어” 하며 좌절할 필요도 없습니다.

모든 사람이 같은 브랜치에 푸시하면 모듈 식으로 코드를 작성하는데도 도움이 됩니다. 기능이 준비되지 않은 경우라도 해당 기능을 사용 중지해서 앱이 Play Store에서 돌아갈 수 있도록 코드를 작성해야 합니다. 백엔드 단에서 기능 플래그를 사용하는 방법은 구현할때 교체가 쉽도록 앱을 설계하고 AB 테스트나 플래그 등으로 기능을 쉽게 켜고 끌 수 있도록 하는 것입니다.

다시 이 방식의 장점을 강조하자면, 이 과정을 통해 항상 “이 문제에 대한 수락 테스트가 필요하고 가능한 많이 테스트로 다룰 수 있도록 해야 해. 배포될 수 없는 코드는 푸시하면 안돼. 내 코드를 모듈화하는 것은 당연한 일이지.” 라는 생각을 기본적으로 해야 합니다. 트리는 정말 쉽고 간결한 모습으로 유지해서, 새 팀원이 들어오면 코드 구성 방식을 하루 안에 파악할 수 있어야 합니다. 이 방식에 대한 제 경험을 이후에 다시 공유하겠습니다.

앞서 말했듯, 충돌 해결이 독립적이고 지엽적으로 일어나므로 저희는 코드 통합 문제를 겪지 않습니다. 어느 시점에서나 항상 배포 브랜치를 조정하거나 견고하게 만들 수 있죠. 모듈화된 방식이므로 기능을 끄는 것도 정말 쉽습니다. 기능이 모듈화돼 있고 교체가 쉬우므로, 일단 배포를 한 이후라도 버그가 있으면 해당 기능을 쉽게 꺼버리고, 회귀 테스트를 다시 수행할 필요없이 배포할 수 있습니다.

또한 지속적인 배치도 가능합니다. 사실 Android에서는 지속적인 배치를 통해 Play Store에 하루에 세네번씩 업데이트를 하지 않기 때문에 큰 문제가 아니긴 합니다. 사용자들이 정말 싫어할 일이죠.

using-git-like-a-pro shazam gitflow

하지만 코드를 커밋할 때마다 3분쯤 후에 실제 제품에 반영될 수 있는 백엔드와 웹 팀에서는 유용합니다. 모든 사람에게 필요한 작업은 아니고 익숙해지는데 시간이 걸리며, 팀 전체가 오랜기간 같이 작업해서 협업에 익숙해야 가능한 방법입니다. 그러니 당장 시도해보지는 마시고 시간을 들여 보세요. 굉장히 유용한 방식인 GitFlow 를 소개합니다.

제가 참조한 원문이 있습니다. 다들 이와 유사한 방식을 사용하긴 하지만, 여기서 공식 명칭을 명명했죠. GitFlow는 아래처럼 세 가지 주요 브랜치를 사용합니다.

  • 실제로 직접 코드를 푸시하지 않는 master 브랜치, 마스터 브랜치는 실제 제품에 적용된 이후 인 배포 커밋을 포함합니다.
  • develop 브랜치에서 실제 작업이 일어납니다. 개발 브랜치는 마스터에 기반하며, 여기서 작고 독립적인 커밋을 실행합니다.
  • 혹은 개발 브랜치에서 feature 브랜치를 분기합니다. 기능 브랜치에서 개발이 실행되기도 합니다.

이 아이디어는 개발에서 브랜치를 분기하고 해당 기능을 만든 후, 기능 브랜치로 개발 브랜치를 머지하여 최신 상태를 유지하고, 준비를 다 끝낸 후 해당 브랜치를 개발 브랜치에 머지해서 테스트하는 것입니다. 병렬로 개발이 진행되는 브랜치가 여러 개라면 이 지점에서 충돌을 해결합니다. 통합 관련 문제가 생길 때 해결하게 됩니다.

개발 브랜치가 배포할 준비가 됐다면 배포 브랜치로 해당 브랜치를 머지해서 지속적인 통합이 되도록 합니다. 이 과정에서 실제 바이너리가 생성되고 테스트되며, 문제가 생긴 경우 핫픽스라고 불리는 해당 브랜치에서 수정하거나, 릴리즈 브랜치에서 핫픽스 브랜치를 만들게 됩니다. 다시 배포로 이 브랜치가 머지되고, 배포는 개발 브랜치로 다시 머지됩니다. 최종 배포인 경우 배포 브랜치를 마스터로 머지하고 태그합니다.

살짝 복잡해보이긴 하지만, 장황하게 설명에 비해 그다지 어렵지 않습니다. 어려운 점은 트리인데요, 새로운 팀에 들어간 경우 이 흐름에 익숙하지 않다면 어렵게 느낄 수 있습니다. 많은 병렬 브랜치가 활성화돼 있고 히스토리를 추적하기가 어렵기 때문입니다. 브랜치들이 최근 머지됐을까? 최신 상태인가? 다른 머지를 실행해야 하나? 어디서 머지해와야 하지? 라는 의문점이 생길 수밖에 없습니다. 이 방식이 복잡하긴 하지만 풀 리퀘스트와 코드 리뷰가 가능하며, GitHub처럼 유명한 도구들은 자동으로 해주기도 합니다.

이제 제가 새로 들어간 ASOS에서 사용하는 브랜치 전략을 소개하고자 합니다. 새 직장에서 전체 프로젝트를 파악하기 위해서는 새로운 코드 베이스에서 작업해야 할뿐만 아니라 혼돈스러울 수 있는 브랜치 전략을 이해해야 하고, 코드를 찾는 것도 까다로울 수 있습니다. 따라서 이 패턴으로 새 팀원을 맞는 것은 조금 어렵긴 하지만, 불가능하지는 않습니다. 이미 도입된지 여러 해가 지나기도 했고, GitHub이나 GitLab 같은 유명 도구들이 이 패턴을 사용하고 있어서 쉽게 풀 리퀘스트를 수행할 수 있습니다.

팁과 유용한 명령어 (27:15)

using-git-like-a-pro tip01

git에는 유용한 명령어가 많지만, GUI 도구를 사용하는 많은 사람들이 이를 모르고 있습니다. git의 강력한 기능에 대해 말해 볼까요? 가상의 저장소가 있고 4개의 커밋이 있다고 가정하겠습니다. 아무 브랜치도 없는 상태입니다.

using-git-like-a-pro tip02

가장 먼저 알려드릴 유용한 명령어는 Rebase 입니다. 리베이스에 대해서는 앞서 말씀드렸는데, 아직 말씀드리지 않은 특별한 버전이 있습니다. Interactive Rebase 라는 겁니다. rebase -i를 실행하면 커밋의 이름이나 관련 이름을 부여할 수 있습니다. HEAD가 현재 커밋을 체크아웃한 곳이며, HEAD~3를 실행하면 현재 헤드에서 3개 커밋 전으로 돌아갈 수 있습니다.

기본적으로는 57909a6에 대한 해시의 축약어이지만 잘 작동합니다. 이를 통해 수정할 수 있는 파일이 만들어지죠. 실제 커밋 메시지를 가진 모든 커밋을 나열해주므로 많은 일을 할 수 있습니다. 첫째로 커밋을 삭제해서 히스토리를 변경할 수 있습니다. 만약 이 파일에서 두번째 줄을 지운 후 저장한다면 git은 이들 커밋을 이 파일에 나타난대로 정렬해서 재적용하려고 할겁니다. 즉, 두번째 커밋이 사라지게 되죠.

모든 커밋이 이전 커밋에 적용되므로 두번째 커밋과 네번째 커밋간에 충돌이 있다면 여기서 해결해야 합니다. 만약 30개의 커밋이 있는 브랜치를 다른 브랜치로 머지하면서 많은 충돌이 발생할 경우 모든 충돌을 즉시 해결해야 합니다. 특히 직접 작업하지 않은 경우 변화를 독립적으로 한정해서 실제 코드가 하려던 역할이 뭔지 파악하는 것이 쉽지 않습니다. 리베이스는 충돌이 있을 경우 실제로 커밋마다 메시지를 표시하므로 커밋 메시지를 쉽게 보면서 코드의 역할을 파악할 수 있습니다.

커밋한 순서가 맘에 들지 않은 경우 순서를 변경할 수도 있습니다. 예를 들어 코드 일부를 고친 후 테스트 실행을 잊은 경우가 있었는데, 저는 이게 정말 싫었습니다. 다시 유닛 테스트를 고치기는 했지만 git 히스토리에는 코드가 변경돼 있었고 유닛도 고정돼 있어서 맘에 들지 않았습니다. 그래서 간단히 순서를 변경했습니다. 이럴 경우 git은 해당 순서대로 커밋들을 리베이스하려고 하는데, 그 과정에서 충돌이 발생한다면 거기서 해결해야 합니다. 사실 이 예시에서 하고 싶은 작업은 아니었죠. 하나의 커밋이 코드 변경 사항과 변경된 유닛 테스트를 담기를 바랬기 때문에 다시 커밋을 squash 했습니다. 내부에서 이처럼 pick을 실행하면 s를 눌러서 squash할 수 있는데, 이는 이전 커밋과 섞는 것이죠. 리베이스 과정에서 git이 이 두 커밋을 가지고 새로운 커밋을 만든 후 새로운 이름을 지정하라는 메시지를 표시합니다.

squash 를 사용하는 방법으로 머지를 하거나 커밋을 수정할 수도 있습니다. git이 수정 사항을 하나하나 처리할 때, 코드를 변경하는 등 원하는 것은 뭐든지 할 수 있습니다. 새 테스트를 작성하고 리베이스를 계속하면, 리베이스가 변경 사항을 적용해서 새 커밋을 만들어 줍니다.

이제 다른 사람들이 이미 체크아웃한 리모트로 푸시된 코드를 계속 작업하는 것은 히스토리를 고치는 것이 돼서 위험합니다. 모든 커밋 해시가 달라져서 git이 파악할 수 없게 됩니다.

Shazam 프로젝트를 받았을 때 실제로 이 방법을 사용했습니다. 몇년 전에 작성된 옛 프로젝트로 이 중 한 부분에 키 스토어와 해당 암호를 포함하고 있어서 맘에 들지 않았습니다. 절대 이렇게 코드를 짜면 안됩니다.

그래서 저희는 많은 팀원이 있는 우리 팀 내 누구도 해당 키스토어와 암호에 접근할 수 없도록 히스토리를 변경하기로 했습니다. 실제 키스토어 정보를 커밋한 최초 커밋을 발견하고 해당 커밋을 지우고 이삼년간에 걸친 전체 git 히스토리를 변경했으며, 모든 이가 안전한 버전으로 체크아웃하도록 했습니다. 이제는 그 모든 일들이 일어난 흔적조차 찾아볼 수 없습니다.

using-git-like-a-pro tip-forcing-label

강제 라벨링. 앞서 말한 대로 브랜치는 실제로 git에서 특별하지 않습니다. 단순히 해시의 이름일 뿐입니다. 따라서 reset --hard 라는 커맨드를 치면 해시 코드나 HEAD~3과 같은 관련 이름을 돌려줍니다. 현재 위치의 라벨을 가져와서 예전 커밋으로 이 라벨을 옮겨버리죠.

예제 그림은 master 브랜치를 아래 커밋으로 옮기는 것을 보여 줍니다. 실제로 HEAD 혹은 현재 위치한 브랜치를 옮깁니다. 하지만 체크아웃하고 있지 않은 다른 라벨을 옮길 수도 있습니다. 브랜치를 이 해시로 옮기라고 git reset --hard라고 명령하면 됩니다.

주로 로컬 커밋에 유용한 방법입니다. 만약 세 개의 커밋이 있는데 하나만 끝나지 않은 상태라 둘만 리모트에 푸시하고 싶다면 현재 마스터를 하나 커밋 뒤로 변경해서 이를 리모트에 푸시할 수 있습니다. 그 다음 마스터를 현재 버전으로 리셋하면 다른 사람의 작업을 방해하지 않겠죠.

using-git-like-a-pro tip-finding lost commit

잃어버린 커밋 찾기. git이 잊지 않고 보존하던 원본 포인트로 돌아가는 방법입니다. git reflog를 치면 참조 로그를 보여줍니다. HEAD가 거쳐간 모든 캐시의 리스트를 출력해주죠. 기본으로는 5개나 10개 정도를 보여줍니다. 작업 시간과 상관없이 지금의 HEAD가 과거에 어디 위치해 있었는지 파악하는데 아주 유용합니다. 브랜치를 자주 옮겨 다니는 작업을 하면서 어떤 이름을 잊거나, 실수로 브랜치를 지운 경우에도 남아있어서 이를 참조할 수 있습니다. 또한 gc로 참조되지 않은 것을 가비지 컬렉팅하도록 실제로 지시할 수 있고 압축을 수행할 수도 있습니다. 이 명령어를 실행하면 모든 참조되지 않은 커밋을 제거할 수 있습니다.

using-git-like-a-pro tip-updating other branches

다른 브랜치 업데이트. 마스터에서 일하면서 페치했는데 다른 사람이 다른 브랜치에 푸시한 경우에 유용합니다. 특히 두 브랜치 모두에서 작업하고 있고 이들 간을 전환하고 있을 때 더욱 유용하죠. 다른 브랜치를 업데이트하는 방법은 주로 그 브랜치로 체크아웃하고 git 페치를 다시 하거나 머지나 리베이스를 해서 라벨을 제대로 된 위치로 옮기고 작업을 계속할 수 있도록 하는 것입니다. 하지만 fetch origin이라고 치고 브랜치 이름을 넣고 쉽표를 넣은 후 다른 브랜치 이름을 넣으면 충돌이 없는 경우 리모트 버전을 가져와서 라벨을 위로 올려줍니다. 빨리 감기를 할 수 있는 셈이죠. 예시 그림에서는 ongoingorigin/ongoing으로 옮겼습니다.

다른 브랜치에서 커밋 가져오기. 마스터만 사용하건, GitFlow만 사용하건 정말 유용한 명령어입니다. 가끔 실수로 잘못된 브랜치에 커밋을 하거나, 제대로 된 브랜치에 넣긴 했지만 아직 머지할 준비가 되지 않은 다른 브랜치에 잘못 넣는 경우가 생깁니다. cherry-pick을 하면 현재 HEAD에다 선별한 해시 코드를 넣어주며, 해당 커밋을 가져다 복사해서 현재 위치의 맨 위에 넣어줍니다. 당연히 변화가 생겼으니 다른 커밋이 되겠죠? 이제 다른 곳을 가리키고 있음을 꼭 기억하세요. 이 선별 과정에서 충돌이 발생한다면 거기서 해결해야 하며, 이제 HEAD는 그 곳이 될 겁니다. git이 이들 커밋이 기본적으로 같은 것임을 기억하고 있으므로 머지할 경우 문제가 발생하더라고 같은 커밋임을 고려해서 불평 메시지를 뱉지 않을 겁니다.

리모트에서 브랜치 삭제하기. 더 이상 필요로 하지 않는 브랜치가 있다면 원격으로도 삭제할 수 있습니다. git branch -d 명령어를 활용하면 로컬 브랜치를 삭제해 줍니다. 그러나 origin 등 리모트 저장소에 푸시했다면 :some_branch처럼 해당 브랜치 이름을 넣으면 리모트로부터 해당 브랜치를 지워줍니다. 물론 이미 체크아웃한 다른 사용자에게는 남아있죠. 그래서 prune라는 명령어를 사용하면 리모트에서 가져와서 내 저장소를 확인하고, 일치하지 않은 것이 있다면 그냥 지워지게 됩니다. 해당 브랜치도 삭제되죠.

using-git-like-a-pro tip-notes

다른 유용한 명령어도 소개하겠습니다. 예를 들어 버그를 수정하는 중 이 버그가 3년 전에 이미 알려졌다면 히스토리에 해당 커밋이 남아 있겠죠. 당연히 다른 모든 사람이 체크아웃한 작업 환경을 바꾸기 싶지 않을테니 이를 변경하고 싶진 않을 겁니다. 옛 커밋으로 돌아가서 노트를 추가하고 싶다면 notes 명령어를 사용하면 됩니다. 사실 많이 복잡하고 버전이 많이 변경됐으며 온라인 문서가 있으니 한번 살펴보시길 바랍니다. 해시 코드를 바꾸지 않고도 옛 커밋에 데이터를 추가할 수 있는 방법입니다. 노트는 실제 커밋의 일부로 취급되지 않기 때문에 원하는만큼 많이 추가할 수 있습니다.

탐색 도 정말 편한 기능입니다. 주로 GUI 도구에서 이 기능을 이용하게 되지만, 사실 커맨드 라인에서도 활용할 수 있는 기능입니다. 히스토리 내의 모든 커밋의 커밋 메시지를 탐색할 수 있는데, 저와 비슷한 유형이라면 모든 커밋을 JIRA 태스크 등의 도구에 넣어둘테죠. 만약 모든 커밋의 티켓 번호를 넣어뒀다면 이 티켓 번호에 영향받는 모든 커밋을 쉽게 찾을 수 있습니다. 아니면 메시지 내용을 찾거나 커밋 내부를 찾을 수도 있고, 다양한 변형도 할 수 있습니다. 특정 문장의 첫 멘션을 찾을 수도 있고 이 코드가 도입된 시점이 언제인지도 간단히 탐색할 수 있죠.

using-git-like-a-pro tip-bisect

마지막으로 버그 발생 시점 찾기 도 가능합니다. bisect는 git이 제공하는 유용한 기능이지만 잘 사용되지 않죠. 저처럼 모든 커밋이 컴파일 가능하고 실행가능한 상황인걸 선호하는 분이라면 언제 이 버그가 생성됐는지 쉽게 찾을 수 있을 겁니다. 버그를 발견하면 세번 이전의 배포에서는 없었음을 알 수 있으니 bisect 할 수 있습니다. git bisect라는 명령어를 사용하면 현재 커밋에 잘못돼 있다고 bad 플래그를 붙이고 커밋 해시나 배포 태그 등을 찾아서 good으로 표시하면서 git에서 다음 과정과 같은 bisect를 실행해 줍니다. 트리의 중간 커밋으로 체크아웃하고 버그가 있는지 체크해본 후 있다면 git bisect bad를 실행합니다. 그러면 git이 다시 bisect할 바른 위치를 찾아주고, 상기 과정을 반복합니다. 6-7번의 단계를 반복하면 몇백 개의 커밋을 거쳐 버그가 생성된 커밋을 찾아낼 수 있습니다.

컨텐츠에 대하여

2016년 7월에 진행한 [360 AnDev](http://360andev.com/) 행사의 강연입니다. 영상 녹화와 제작, 정리 글은 Realm에서 제공하며, 주최 측의 허가 하에 이곳에서 공유합니다.

Savvas Dalkitsis

Android geek, TDD nut, International Speaker, ex Shazamer, currently a Lead Android Developer at ASOS. Passionate about software quality, best practices and new technologies. E-mail: savvas.dalkitsis@gmail.com

4 design patterns for a RESTless mobile integration »

close