저는 깃으로부터 저주받았습니다.
사실 처음부터 깃(Git)으로부터 저주 받은 것은 아니었습니다. 깃은 그저 세계에서 가장 널리 사용되는 것처럼 보이는 형상관리도구 중 하나이고 세계에서 가장 큰 중앙집중식 형상관리도구 서비스인 깃헙이 사용하는 서비스이기도 하며 인터넷에서 만날 수 있는 오픈소스 프로젝트 거의 대부분이 깃을 사용하고 있는 그런 나와 직접 관계가 있지는 않지만 가끔 뭘 좀 하려면 마주치지 않을 수는 없는 그런 존재였습니다. 저를 제외한 전 세계의 모든 사람들이 형상관리도구로 깃을 사용할 때 제가 속한 프로젝트에서는 항상 SVN이나 퍼포스를 사용했는데 종종 인터넷에서 만나는 분들의 글을 보면 퍼포스가 얼마나 미개한지를 설파하고 있었습니다.
오래 전에 도쿠위키를 사용하며 호스팅 환경을 직접 관리할 때가 있었는데 다양한 플러그인을 설치하고 또 몇몇 기능을 직접 수정해서 사용하고 있었기 때문에 최신 업데이트가 돼도 소스를 바로 적용할 수가 없었습니다. 그래서 도쿠위키의 최신 버전을 포크해 온갖 플러그인을 설치한 상태를 그대로 커밋 해 소스 상태를 유지하고 그 상태에 다시 수정사항을 반영해 사용하다가 최신 버전이 나오면 브랜치를 만들어 그안에서 정상 동작할 때까지 머지와 수정을 반복해 멀쩡해지면 메인 브랜치에 반영하곤 했습니다. 회사에서는 퍼포스를, 집에서는 이런 식으로 깃을 사용했는데 서로 빌드의 스냅샷을 바라보는 시각이 달라 회사에서 마인드셋과 집에서 마인드셋을 바꿔야만 서로 다른 상황을 납득하고 적응할 수 있었던 것 같습니다.
이전에 기술적인 배경이 약한 입장에서 기획자가 깃에 대해 불평하기, 익숙한 습관과 깃이란 이야기를 한 적이 있고 또 한번은 언리얼 기반 개발과 깃은 잘 어울리지 않는 것 같음이라는 이야기를 한 적도 있는데 이제 곧 언리얼 엔진 개발을 깃에 기반해 진행한 지 2년이 되어 가는데 지금도 깃은 이전에 비해 전혀 개선되지 않았습니다. 하지만 여전히 인터넷에서 만나는 ‘모든’ 사람들은 자신들에게는 깃이 아무런 문제 없이 동작하며 깃의 문제라기보다는 LFS 문제이거나 윈도우 OS가 문제이거나 커맨드라인을 사용하지 않아 생기는 문제이거나 윈도우 클라이언트의 문제이거나 바이너리 파일이 문제이거나 프로젝트가 너무 무겁거나 표준 깃이 아닌 깃헙의 문제이거나 안티바이러스 소프트웨어 문제이거나 깃의 동작을 잘 이해하지 못한 사용자 문제일 가능성을 이야기하곤 했습니다.
그래서 어느 날 깃이 또 index.lock
파일을 스스로 삭제하지 않으며 모든 작업을 거부하고 드러눕자 이 모든 문제는 사실 그동안 깃을 욕해 온 내가 깃에 의해 저주를 받은 결과라는 사실을 믿지 않을 수 없었습니다. 깃은 죄가 없고 깃은 완벽한 형상관리 도구이지만 그런 깃을 제대로 알지도 못한 채 욕을 해 온 결과 저는 깃의 저주에 빠져 깃은 가장 간단해 보이는 동작 조차 제대로 해 내지 못하고 시시각각 에러 메시지를 토해 내며 일상을 고통스럽게 만들고 있습니다. 저는 깃으로부터 저주받았습니다.
깃이 저에게 내린 첫 번째 저주는 풀(Pull)에 실패하는 겁니다. 포크라는 윈도우용 비주얼 클라이언트를 사용하는데 네트워크 사정에 따라 풀에 실패하고 풀 하다 실패한 모든 파일을 로컬 체인지에 올려 놓고 멈춰 버립니다. 처음에는 그냥 다시 풀을 재시도하면 작업을 이어서 할 줄 알았는데 첫 번째 풀에 실패한 깃은 이 풀에 성공했더라면 다운로드 해서 로컬에 덮어 썼을 모든 파일을 로컬 체인지에 올려놨고 다시 한 번 풀을 시도하려 할 때 이 파일을 커밋하든지 치우든지 하라며 풀을 거부합니다. 제가 바꾸지도 않은 온갖 파일들이 로컬 체인지에 등록되어 무슨 변경사항이 있나 비교해 보면 아무 변화도 없습니다. 이런 상황이 생기는 이유는 아마도 깃이 먼저 새 파일의 해시를 받아 와 저장한 다음 파일 본체를 받기 시작하는데 네트워크든 뭐든 어떤 이유로 파일 본체를 받기 시작하는데 실패하면 작업을 멈추고 그 멈춘 상태를 평가해서 새로 받은 해시와 아직 받지 못한 ‘현재’ 파일 상태를 비교해 ‘현재’ 파일들이 모두 변경되었다고 판단하고 모두 로컬 체인지에 올려 버린 것입니다.
이 상황을 해결하는 방법은 로컬 체인지에 나타난 파일은 그냥 업데이트에 실패한 파일이라 변경을 취소(Discard)하고 다시 풀을 하면 됩니다. 이번에도 똑같이 풀에 실패하지 않기를 기도하면서요. 그런데 상황에 따라 같은 상황이 연속으로 여러 번 반복되기도 하는데 이 때는 한 번에 최신으로 풀 하려 하지 말고 한 번에 커밋 하나 씩 풀 하기를 반복하면 이런 문제를 회피할 수 있습니다. 이런 현상이 생기는 원인을 추측해 보면 로컬 최신에서 리모트 최신을 풀 할 때 이 사이에 받아 올 파일 수가 많고 파일 용량이 크면 한 번에 많은 파일 다운로드에 실패해 해시만 받아오고 파일 본체를 못 받아오는 상태가 되는 것 같습니다.
그런데 뭔가 문제가 생길 때 기술적인 배경이 약한 사람들이 바로 다음에 이어서 할 동작이 뭔지 한번 생각해 봅시다. 작업에 실패했으면 바로 그 작업을 다시 시도합니다. 웹사이트가 ‘사용량이 많으니 잠시 후 다시 시도해 주세요’ 라고 말하면 우리들은 그 메시지가 끝까지 표시되기도 전에 바로 새로고침을 시도하는데 깃이라고 해서 다를 이유가 없습니다. 하지만 깃은 이렇게 한 번 잘못 되면 내 손으로 직접 로컬 체인지를 삭제하기 전까지는 재시도를 금지하는데 이는 직접 트러블 슈팅을 하기 어려운 분들께 공포스러운 상황으로 다가옵니다.
깃이 저에게 내린 두 번째 저주는 스스로 락 파일(index.lock)을 '안' 지운 채 이 파일이 남아 있어 아무 일도 할 수 없다며 역시 모든 시도를 거부하는 증상을 시시때때로 보이는 것입니다. 깃은 아마도 파일 뭉치로 된 데이터베이스에 현재 상태를 기록하는 것 같은데 그냥 파일시스템에 현재 상태를 기록하고 있을 뿐이므로 다른 소프트웨어가 이 파일에 접근해 내용을 고쳐 동작을 망가뜨릴 가능성이 있습니다. 또 같은 로컬 리파지토리에 여러 깃 클라이언트가 접근하는 상황을 막지 않기 때문에 여러 깃 클라이언트가 동시에 같은 리파지토리에 쓰려고 하는 상황이 일어날 수 있습니다. 그래서 어떤 깃 클라이언트가 작업을 시작하면 락 파일을 남겨 다른 깃 클라이언트가 이 리파지토리에 쓰기를 시작하려다가 락 파일을 발견하면 다른 깃 클라이언트가 작업 중이라고 생각하고 수행하려는 작업을 멈춘 채 락 파일이 사라질 때까지 작업을 거부합니다.
그런데 윈도우용 깃 비주얼 클라이언트 상당수는 깃을 내장해 다른 깃 클라이언트가 윈도우 상에 존재하지 않는데도 락 파일을 발견하고는 자신이 만든 락 파일이 아니라며 모든 작업을 거부해 버립니다. 시스템에는 이 깃 비주얼 클라이언트 이외에는 깃 비슷한 그 어떤 것도 설치하지 않았으니 그 락 파일을 생성한 것은 이 비주얼 클라이언트 또는 이 비주얼 클라이언트가 사용하는 시스템 깃 혼자 뿐일 텐데 도대체 다른 누가 락 파일을 생성한 것인지 모를 노릇입니다. 심지어 같은 깃 비주얼 클라이언트로 10초 전에 시도했던 풀에 실패해 로컬 체인지를 삭제하고 다시 풀을 시도하려던 참인데 심지어 깃이 삭제하지 않고 버티는 그 락 파일의 생성 시간은 바로 10초 전입니다. 깃 스스로 락 파일을 생성한 다음 작업을 수행하다가 실패하면 락 파일을 삭제하지 않고 갑자기 스스로 생성한 락 파일을 보곤 ‘아시발깜짝이야’라며 모든 작업을 거부하고 바닥에 드러 누워 버립니다.
이 상황을 해결하는 방법은 내가 직접 리파지토리 디렉토리에 가서 숨겨져 있는 .git
디렉토리를 연 다음 당당히 버티고 있는 index.lock
파일을 삭제하는 겁니다. 자기 자신이 생성해 놓고 이를 모른 체 하며 내가 파일을 직접 지워 주기 전까지 아무 것도 안 하는 이 소프트웨어의 동작은 저 한 명에게만 내린 저주가 아니고서는 설명할 수가 없습니다.
이 락 파일 저주는 해결 방법이 간단한 반면 이 저주인지 아닌지 눈치채는데 시간이 걸리는데 이유는 깃이 락 파일이 남아 있어서 작업을 거부한다는 직접적인 원인을 에러 메시지에 내보내지 않고 엉뚱한 메시지를 내보내기 때문입니다. 락 파일을 삭제하면 끝나는 간단한 트러블 슈팅 방법을 확인하는데 시간이 걸린 이유는 에러 메시지에는 System.IO
어쩌구가 실패했다고 나오기 때문입니다. 저 메시지를 보고 기술적 배경이 없는 사람들은 도대체 무슨 일을 해야 할까요. 에러 메시지를 복사해 검색해 보면 안티바이러스 소프트웨어를 끄라느니 윈도우 디펜더가 문제라느니 NTFS가 이상하다느니 온갖 헛소리만 가득하고 정작 그래서 문제를 해결하는 방법을 알려주는 사람은 있지도 않습니다.
결국 파일시스템을 조작해 파일을 삭제하는데 실패했다는 이야기처럼 보이는데 그 순간 파일시스템에 맹렬히 읽고 쓰는 드랍박스나 메모리를 수 기가 씩 먹고 웹 브라우징의 무거움을 한껏 뽐내는 크롬 브라우저나 GPU 팬으로 양력을 생성해 날아오르기 직전으로 만드는 언리얼 엔진도 다들 평화롭게 파일을 읽고 쓰는 상황에 왜 깃만 이 난리일까요. 결국 이는 깃이 저를 저주했기 때문입니다.
한 번도 본 적은 없지만 깃이 갓벽하게 동작하는 세계에서는 모든 파일이 1메가 이하이고 모든 파일이 텍스트 형식이며 네트워크는 빛의 속도로 동작하고 온갖 서비스들이 완벽하게 동작하고 있겠지만 제가 사는 세계에서는 매주 열 몇 개씩 집행하는 이벤트 배너 하나가 13메가이고 외주사로부터 납품 받은 새 사운드이펙트 파일이 400메가이며 실제 게임을 구동하는데 필요한 파일만 포함한 배포용 빌드도 몇 기가에 달하는데 깃에 이런 ‘일상적인’ 파일을 올리기 위해서는 LFS라는 추가 기능이 필요합니다. 애초에 깃이 제게 내린 가장 큰 저주는 거대한 바이너리 파일이 하루에도 몇 기가 씩 증식하는 세계에서 깃을 사용할 수밖에 없게 만든 그 자체에 있지 않을까 싶습니다.
그런데 웃긴 점은 심지어 깃을 내장한 윈도우 클라이언트 조차 LFS는 마치 깃이 아닌 것처럼 행동하곤 합니다. 가령 Lock
기능은 여러 사람이 작업하는 환경에서 바이너리 파일을 편집하는데 굉장히 중요한데 이 기능은 당연히 파일이나 히스토리의 컨텍스트 메뉴에서 찾을 수 있어야 할 것 같지만 거의 모든 깃 윈도우 클라이언트에서 이 기능은 LFS
메뉴 밑에 있습니다. 형상관리도구 깃을 사용하는 사람 입장에서 LFS가 뭔지 알게 뭔가요. 마치 새로 산 자동차의 인포테인먼트 시스템 메뉴에 ‘음악’, ‘내비게이션’, 드라이빙 모드'처럼 기능 이름으로 메뉴가 이어지다가 갑자기 다른 회사에서 만든 기능을 한 곳에 모아 둔 ‘마이크로소프트’ 같은 메뉴가 튀어나오는 느낌입니다.
퍼포스는 로컬에 새 리비전을 업데이트 할 때 새 리비전을 파일 단위로 관리합니다. 제 로컬에는 마지막으로 업데이트 한 프로젝트 전체의 최신 파일만 저장되며 나머지 히스토리는 서버에 있습니다. 각 리비전은 파일 단위로 관리되어 만약 내가 수정하고 있지 않은 다른 파일을 누군가 업데이트 했다면 그냥 그 파일이 있는 경로를 업데이트 하기만 하면 됩니다. 그러면 서버에서 업데이트 된 경로에 해당하는 파일을 받아 로컬에 덮어 쓰고 끝납니다. 저는 기존에 열어 뒀던 작업 환경에 아무 것도 손대지 않고 그냥 새 부분만 받을 수 있습니다.
그런데 깃은 새 버전에 의해 파일 일부만 변경되더라도 로컬 리파지토리 전체 변경을 요구하는데 리파지토리 안에 실행파일이 있고 이 파일이 실행 중이라면 새 버전에 이 파일에 대한 변경사항이 없어도 풀을 거부합니다. 언리얼 에디터 바이너리를 실행하고 있다면 에디터가 수정되지 않는 버전을 풀 하더라도 에디터를 종료하고 풀 하고 다시 에디터를 실행해야 할 수도 있는데 이 이터레이션을 거칠 때마다 몇 분이 사라집니다.
깃은 분산 어쩌구 환경이어서 모든 사람들이 히스토리 전체를 들고 있기 때문에 네트워크에 문제가 생겨도, 형상관리도구 서버에 문제가 생겨도 문제 없이 작업할 수 있는 장점이 있다고 합니다. 그런데 이게 장점인가요? 서기 2023년 여름 기준으로 생각을 좀 해봅시다. 현대 인터넷 환경에서 네트워크가 단절된 상태에서 일어난 작업은 작업의 유효성을 검증할 수 없을 수 있습니다. 네트워크 단절 상태에서 무슨 짓을 했는지, 복귀 후 올라온 리비전의 정합성을 어떻게 장담할 수 있을까요? 또 현대 클라우드 환경에서 서버에 문제가 생겨도 히스토리 전체에 접근할 수 있는 특징이 장점이라고 말할 수 있나요?
현대에 형상관리도구 서버는 그냥 공기처럼 존재해야 합니다. 혹시 서버에 문제가 생길 경우에 대비해 모든 작업자들에게 모든 히스토리를 분산 시키는 것은 마치 지구에 산소가 갑자기 사라질 지도 모르니 모든 사람이 만약을 대비해 항상 산소 마스크를 착용하고 다니는 것과 별로 다르지 않습니다. 지구에 산소가 사라지면 그냥 인류의 종말입니다. 산소가 없는 지구에서 산소 마스크를 쓴 채 홀로 살아 남아 무슨 의미를 찾을 수 있을까요.
오히려 이 특징은 게임 개발에 굉장히 이상한 상황을 만드는데 이 상황을 한번 상상해 봅시다. 현대에 스팀에서 아무 AAA 게임을 찾아 구입한 다음 다운로드 하려고 보면 100기가 이상의 빠른 스토리지를 요구하는 경우를 쉽게 찾을 수 있습니다. 이 100기가는 개발팀에서 실제 고객에게 전달되어야만 하는 파일을 별도로 빌드한 결과이고 이 100기가어치 게임을 개발하기 위해 지난 수 년에 걸쳐 형상관리도구에 올린 파일 크기는 이보다 훨씬 큽니다. 퍼포스 체인지리스트 번호 자릿수가 일곱 자리가 넘는 어떤 프로젝트의 전체 스토리지 크기는 수 십 테라에 달했는데 퍼포스를 사용하고 있었기 때문에 각 작업자들은 최신 리비전의 집합으로 구성된 훨씬 작은 스토리지만 있어도 작업에 참여할 수 있었습니다.
하지만 만약 같은 프로젝트가 깃을 사용하고 있었다면 단지 팀에 온보딩 해 언리얼 에디터를 한번 실행해 보기만 하고 싶어도 수 십 테라 어치 스토리지를 로컬에 들고 있어야 합니다. 깃 관점에서 이 상황은 전혀 이상하지 않을 수 있지만 윈도우와 언리얼 기반으로 게임 만드는 사람 관점에서는 이상합니다. 스토리지가 아무리 저렴해도 모든 사람들이 배포용 빌드만 100기가인 프로젝트의 히스토리 전체를 가지고 있는 것이 올바른가요?
깃이 저에게 내린 또 다른 저주에는 갓벽한 분산환경의 장점을 마치 신의 존재처럼 증명할 수는 없지만 이해하고 믿어야만 하는 것입니다. 분산환경이라는 깃의 특징을 이해하기 위해서는 겉으로는 머지 처럼 보이지 않고 또 겉으로는 브랜치 처럼 보이지 않지만 실제로는 머지이고 브랜치인 환경에서부터 시작해야 합니다. 아무 것도 하지 않고 그냥 리파지토리를 생성한 다음 그걸 로컬로 풀 해 오면 아무 것도 안 했지만 이미 브랜치 두 개를 관리해야 합니다. 로컬 메인 브랜치와 리모트 메인 브랜치입니다. 사실상 풀은 리모트 브랜치를 로컬 브랜치에 머지 하는 행동이고 푸시는 그 반대 행동입니다. 이 개념이 이상하지 않다고 생각할 수 있지만 퍼포스에서 단순히 리모트 파일을 ‘업데이트’하고 로컬 파일을 ‘커밋’하는 행동과는 완전히 달라 같은 행동을 깃에서는 파일을 스테이징 하고 커밋 하고 푸시 해야 간신히 ‘커밋’ 하나에 해당하는 행동을 한 것과 같습니다.
머지와 리베이스는 또 어떤가요? 깃을 처음 보며 암만 생각해도 이상하다고 생각한 개념이 리베이스인데 브랜치는 최대한 작게 만들어 빠르게 목적을 해결하고 메인 브랜치에 머지해 통합된 상태를 유지해야 합니다. 그런데 아직 작업이 끝나지 않아 메인에 머지할 수 없는 상태인 브랜치에 메인의 최신 변경사항을 반영하기 위해 리베이스를 한다니 좀 이상하지 않나요? 일단 메인 브랜치에 머지할 수 없는 상태로 메인의 변경사항을 가져와야 한다면 근본적으로 브랜치 전략의 실패입니다.
그리고 정말 메인 브랜치의 변경사항이 필요하다면 더 단순한 접근은 현재 브랜치에서 메인에 머지될 수 있는 모양으로 작업을 마무리하고 메인에 머지한 다음 그 커밋을 기준으로 다시 브랜치로 돌아오는 것입니다. 브랜치가 커져 머지가 어렵게 된 브랜치 전략의 실패를 만회할 수단으로 리베이스와 머지를 구분하면서 ‘서버에서 파일을 받아 로컬에서 수정하고 다시 올린다’는 단순한 절차를 여러 브랜치와 풀과 푸시와 머지와 리베이스의 용도와 차이를 익혀야만 개발에 참여할 수 있는 이상한 상태가 되어 버렸습니다.
전혀 과장하지 않고 하루에 이런 문제를 다섯 번 씩은 겪으며 트러블 슈팅을 한 번 할 때마다 몇 분 씩 우주 저 편으로 사라지고 있습니다. 물론 회사는 그렇게 고작 깃 트러블 슈팅 따위에 날리며 소모하는 시간에도 급여를 지급하므로 최악의 상황은 아닙니다. 하지만 그럴 때마다 프로젝트 런칭과는 조금씩 멀어지며 이런 문제를 겪는 사람이 저 한 명 뿐이 아니라면 이는 결코 무시할 수 없는 손해라고 생각합니다.
하지만 다시 한 번 강조하건데 깃은 수많은 사람들과 수많은 환경에서 완벽하게 동작하고 있으며 저와 제 주변 사람들이 겪는 이런 문제들은 단 한 번도 본 적 없는 문제일 뿐이며 이는 깃이 저를 포함한 우리들에게 저주를 내린 결과입니다.
깃은 세계적으로 가장 널리 활용되는 분산형 형상관리도구로써 네트워크의 불안정이나 서버의 유실 같은 심각한 재해 상황에도 불구하고 엄청나게 강력하고 안전하게 리파지토리를 보호하며 리눅스 환경에서 완벽하게 동작하고 텍스트 몇 글자면 전달할 수 있는 메시지에 굳이 거대한 바이너리를 사용해 리파지토리를 수 테라로 늘리는 미개한 프로젝트들을 위해 LFS를 준비해 주긴 했지만 LFS는 깃이 아니므로 LFS에서 일어나는 모든 문제는 LFS의 문제일 뿐 깃의 문제가 아니며 깃은 깃을 잉태한 바로 그 사람이 만들어낸 리눅스에서 가장 완벽하게 동작하고 윈도우에서 실행은 되지만 동작을 보증하지 않으므로 이 점이 마음에 안 들면 윈도우에서 깃을 실행하려는 무모한 생각을 해서는 안되며 이 모든 조건 중 어느 하나라도 어기면 즉시 index.lock
을 삭제하지 않아 아무 작업도 할 수 없는 저주에 빠지게 됩니다.
저는 깃의 저주에 빠졌고 이 저주로부터 영원히 빠져나올 수 없을 겁니다.