Lobsters

On Git Refs

Git 참조(refs)를 이해하는 법

Git은 변경되지 않는 콘텐츠 주소 기반 객체 저장소뿐 아니라, 문자열 키로 객체를 가리키는 변경 가능한 참조(ref) 저장소도 사용합니다. 브랜치와 태그가 ref라는 점을 바탕으로 fetch 인수와 로컬·원격 브랜치의 차이를 풀어 설명합니다.

AI 요약

Git을 커밋 해시와 스냅샷으로 이뤄진 콘텐츠 주소 기반 데이터베이스로만 이해하면, origin master와 origin/master처럼 비슷해 보이는 표기가 왜 다른지 헷갈릴 수 있습니다. 글쓴이는 Git에 변경되지 않는 객체 저장소 외에도 문자열을 키로 삼고 객체를 값으로 가리키는 변경 가능한 키-값 저장소가 있다고 설명합니다. 이 키가 바로 ref이며, 브랜치와 태그, Git notes도 모두 ref입니다. 저장소의 .git/refs 디렉터리를 살펴보면 ref의 구조를 확인할 수 있습니다.

fetch 인수의 의미

Git 명령은 ref 이름을 줄여 쓰고 기본 인수를 생략하므로, 명령줄에서 어떤 인수가 ref인지 바로 알아보기 어렵습니다. 글은 약칭을 모두 풀어 쓰면 구조가 선명해진다고 설명합니다. git fetch의 첫 인수는 원격 저장소의 위치입니다. origin은 매번 URL을 입력하지 않도록 저장소에 붙인 이름이며, 흔히 주 원격 저장소를 가리킵니다.

두 번째 인수는 원격 ref와 로컬 ref를 짝짓는 source:target 형식입니다. 원격의 refs/heads/master를 가져와 로컬의 refs/remotes/origin/master라는 이름으로 저장하라는 뜻입니다. 원격 ref 이름을 그대로 로컬에서 쓰면 여러 원격 저장소의 브랜치 이름이 충돌할 수 있습니다. 그래서 origin 같은 원격 이름 아래에 로컬 ref를 구분해 둡니다. Git은 보통 ref 이름의 접미부만 적도록 허용하며, 원격 브랜치 ref의 heads를 로컬의 remotes/$remote로 대응하는 규칙도 기본으로 제공합니다.

로컬 브랜치와 원격 추적 브랜치

Git은 오프라인 우선으로 동작하며, 네트워크가 연결될 때마다 자동 동기화하지 않습니다. 사용자가 fetch나 push를 실행할 때 원격과 데이터를 주고받습니다. 따라서 refs/remotes/origin/main은 원격의 main 자체가 아니라, 마지막으로 동기화했을 때 원격 상태를 기록한 로컬 ref입니다. 로컬 브랜치 refs/heads/main은 처음에는 이 ref와 같은 커밋을 가리킬 수 있지만, 로컬에서 커밋하면 로컬 브랜치만 앞으로 이동합니다.

그 사이 원격의 main도 갱신됐다면 push가 충돌할 수 있습니다. 이때 Git은 원격 상태를 반영해 로컬의 refs/remotes/origin/main을 갱신하지만, 사용자는 로컬 브랜치 refs/heads/main을 그에 맞춰 정리한 뒤 다시 push해야 합니다. 두 ref가 모두 로컬에 있지만 서로 다른 상태를 나타낸다는 점이 중요합니다.

브랜치 생성 명령의 약칭

예시의 fetch 명령은 .git/config에서 origin의 URL을 찾아 네트워크 요청을 보내고, 원격 refs/heads/master에 해당하는 커밋과 조상 커밋을 가져와 로컬 refs/remotes/origin/master를 갱신합니다. 이어지는 브랜치 생성 명령에서 origin/master는 시작점을 가리키는 ref의 약칭입니다. 새 로컬 브랜치 refs/heads/my-feature가 이 지점에서 만들어집니다.

다만 명령의 첫 인수처럼 새 ref를 생성하는 인수라고 해서 그 인수 자체가 ref인 것은 아닙니다. 이 구분을 놓치면 모든 명령 인수를 ref 이름으로 풀어 쓰려다 혼란이 생깁니다. 글은 Git의 ref 구조와 약칭 규칙을 이해하면 origin/master가 원격 저장소에 있는 브랜치가 아니라, 마지막 fetch 때 확인한 원격 상태를 담은 로컬 ref임을 알 수 있다고 정리합니다.

Lobsters 반응

  • @pervognsen — “Git에 변경 불가능한 추가 전용 콘텐츠 주소 기반 데이터베이스뿐 아니라, 평범한 변경 가능 키-값 저장소도 있다는 걸 이제 이해했습니다.” 저는 2007년에 나온 Scott Chacon의 Git Internals 책으로 Git을 배웠습니다. 시스템 프로그래머처럼 밑바닥부터 작동 방식을 배우고 싶다면 좋았습니다. 책 21쪽쯤에는 Git 객체는 불변이지만 참조는 계속 바뀔 수 있다고 나옵니다. 참조는 특정 커밋을 가리키는 단순 포인터이며 브랜치와 원격 저장소가 그 예입니다. 덧붙이면, 불변 객체 저장소 위에 변경 가능한 키-값 저장소를 만드는 최소 설계에는 ref 하나면 됩니다. 영속 저장소가 ref를 원자적으로 갱신하지 못한다면 장애 복구를 위해 이전 루트와 새 루트, 두 개를 둘 수 있습니다. 복구 과정에서 두 루트 설명자를 읽고 논리 타임스탬프가 더 최신이며 체크섬도 통과한 쪽을 선택합니다. 임베디드 데이터베이스 lmdb는 콘텐츠 주소를 쓰지는 않지만, 번갈아 쓰는 루트 페이지 두 개로 비슷한 설계를 구현합니다.
    • @contificate — 덧붙인 이야기와 관련해, Git 호환 콘텐츠 주소 저장소 위에 키-값 저장소를 구현한 재미있는 프로젝트로 Irmin(https://irmin.org/)이 있습니다. Git의 아이디어를 다른 소프트웨어에서도 더 많이 활용하지 않는 점이 아쉽습니다.
    • @pervognsen — 저도 대규모 빌드의 기반으로 직접 만든 시스템을 쓰고 있습니다. 원자적인 트리 스냅샷과 재귀적으로 파생되는 빌드 산출물을 다루며 Irmin과 닮은 부분이 있습니다. 제가 갖추지 않았고 필요하지도 않은 주요 기능은 Irmin의 병합 가능한 복제 데이터 타입입니다. 제 생각에 Irmin에서 가장 눈에 띄는 기능입니다: https://www.youtube.com/watch?v=_1zFtB4HSwU. 제 시스템의 사용자 정의 데이터 타입 지원은 객체 그래프를 일반적인 방식으로 순회하는 데 필요한 최소한에 그칩니다. 객체에는 본문인 블롭과 함께, 형식이 통일된 다른 객체 링크 배열이 있습니다. 제가 보기에 Git을 범용 객체 저장소로 쓸 때 단점은 가비지 컬렉션 같은 기능이 정해진 객체 종류를 알아야 하고, 링크를 따라가려면 종류별 형식도 파싱해야 한다는 점입니다.
  • @sammko — 원격 저장소마다 fetch와 push의 기본 refspec을 설정할 수도 있습니다. 이 페이지가 좋습니다: https://git-scm.com/book/en/v2/Git-Internals-The-Refspec
  • @chriswarbo — 맞습니다. 분산형·P2P Git 원격 저장소를 시험하면서 두 가지 구성 요소가 필요했습니다. 불변 객체 저장소로는 IPFS를, 변경 가능한 ref 저장소로는 pkarr을 골랐습니다. 원칙적으로 변경 가능한 저장소에 객체도 넣을 수 있지만 대체로 잘 맞지 않습니다. 불변 저장소에서는 요청한 해시와 콘텐츠의 해시를 비교하면 되므로 서명이 필요하지 않습니다. 반면 pkarr이나 DNS 같은 변경 가능한 시스템은 레코드 크기 제한이 작아서 여러 요청으로 나눠야 합니다.
  • @chriswarbo — 직장에서 유용하게 쓰는 방법 하나는 로컬 master나 main 브랜치를 만들지 않는 것입니다. 기능 브랜치로 작업하므로 origin/master에서 새 브랜치를 따고, 나중에 fetch로 새 버전을 받은 뒤 그 기준에 맞춰 rebase 등을 합니다. 대부분은 원격 추적 브랜치를 위한 로컬 브랜치를 늘 만들지만, 제 경우에는 그럴 필요가 없습니다. 오히려 둘을 동기화하느라 복잡해집니다. origin/master도 로컬에 있다는 점을 알면 됩니다. 마지막 fetch를 바탕으로 내 저장소가 원격에 있다고 판단하는 상태입니다.

원문: matklad.github.io / 번역·요약: Trawling