서로 다른 브랜치의 갱신까지 한 줄에 세우면 느린 쓰기 하나가 무관한 작업을 기다리게 한다. GitHub가 설명한 ref 단위 직렬화는 그 대기 범위를 줄인다. 같은 main에 집중되는 갱신, 오래된 기준 커밋, 함께 바뀌어야 하는 여러 ref는 여전히 따로 다뤄야 한다. 로컬 Git의 작은 상태 전이 실험으로 이 차이를 살펴본다.
에이전트 네 개가 만든 변경은 어디에서 줄을 설까
문서 수정, 테스트 추가, 번역, 의존성 갱신을 네 에이전트에 맡겼다고 하자. 각 작업은 자기 브랜치에서 끝났는데 저장소로 보내는 단계가 느리다. 이때 필요한 질문은 “에이전트를 더 늘릴까”보다 서로 다른 작업이 같은 순서를 기다리고 있는가다.
GitHub는 10월 21일 공개한 인프라 해설에서, 리전 간 왕복이 약 25ms인 환경의 저장소 전체 잠금이 쓰기를 제약한다고 설명했다. 기존 구조는 커밋에 과반수면 충분해도 다음 진행 전에 건강한 복제본들의 응답을 제한 시간까지 기다린다. 변경 방향은 저장소 전체에서 개별 ref로 직렬화 범위를 좁히는 것이다. 다른 ref의 쓰기는 함께 진행할 수 있지만 각 쓰기의 리전 간 지연은 남는다. 이는 공급자의 설계 설명이며 모든 저장소의 전환 완료나 독자의 push 속도를 보장하는 발표가 아니다. GitHub 공식 해설
이 글은 그 변화가 작업 분할에 주는 의미를 분석한다. 아래 대기 시간은 설명용 계산이고, 뒤의 Git 결과는 임시 로컬 저장소에서 직접 확인한 것이다. GitHub의 분산 합의나 네트워크 성능을 재현하지 않았다.
브랜치는 파일 묶음의 복사본보다 움직이는 이름에 가깝다
Git의 브랜치 ref는 커밋을 가리키는 이름이다. 예를 들어 refs/heads/main이 커밋 A를 가리키다가 새 변경 뒤 B를 가리킬 수 있다. 이름을 기억하면 긴 객체 식별자(OID)를 매번 외우지 않아도 이력의 시작점을 찾는다. Git 내부를 설명하는 Pro Git의 References 장이 이 관계를 보여 준다.
이름과 내용을 나누면 동시 작업의 문제가 선명해진다. 다음은 설명을 위한 상태다.
기준 커밋: A
문서 작업 결과: B (A에서 갈라짐)
테스트 작업 결과: C (A에서 갈라짐)
서로 다른 이름: docs → B, tests → C
공유하는 이름: main → B 또는 C 중 무엇인가?
첫 경우에는 두 결과를 각 이름에 보관할 수 있다. 두 번째 경우에는 main의 다음 값을 정해야 한다. B와 C가 서로 다른 파일을 수정했더라도 main이라는 이름은 하나다. 파일 내용의 병합 가능성과 ref 갱신의 충돌은 서로 다른 질문이다.
git update-ref <ref> <new-oid> <old-oid>는 현재 값이 예상한 옛 OID와 맞는지 확인하고 바꾸는 명령이다. 이 비교 덕분에 “내가 A를 읽고 일하는 동안 누군가 이미 main을 바꿨다”는 사실을 놓치지 않을 수 있다. Git 2.48 문서
같은 네 건도 분포에 따라 완료 시점이 달라진다
한 갱신이 끝날 때까지 60ms 걸리고, 네 요청이 시각 0에 함께 도착한다고 가정하자. 서로 다른 ref에는 충분한 처리 자원이 있으며 추가 조정 비용은 없다고 둔다. 아래 값은 이 가정만 계산한 것이며 25ms 왕복을 이용해 GitHub의 쓰기 시간을 추정한 값이 아니다.
| 갱신 대상과 직렬화 단위 | 마지막 요청 완료 | 무엇이 대기를 만드는가 |
|---|---|---|
| 저장소 하나에 네 건을 순차 처리 | 240ms | 서로 다른 ref도 앞 작업 뒤에 배치 |
| 네 ref에 한 건씩, ref별 처리 | 60ms | 각 ref에는 앞선 요청이 없음 |
| main에 세 건, 다른 ref에 한 건 | 180ms | main의 세 건은 여전히 순차 처리 |
이 가정에서는 첫 줄이 4 × 60, 마지막 줄이 max(3 × 60, 1 × 60)이다. 그림은 실제 추적 화면이 아니라 이 세 번째 분포를 설명한다. 가로로 이어진 main의 작업은 순서를 기다리고, 다른 브랜치의 작업은 별도 줄에서 끝난다.
따라서 에이전트마다 작업 브랜치를 주는 설계에는 독립적으로 결과를 보관할 자리를 만든다는 의미가 있다. 하지만 모든 변경을 최종 main에 합치는 과정까지 네 배 빨라진다는 결론은 나오지 않는다. 병합 검사, 테스트, 승인, 배포가 한곳에 모이면 그 단계가 다음 대기가 된다. 브랜치만 늘리고 작업 사이의 의존성을 그대로 두면 병렬화 효과도 제한된다.
실제 시스템에서는 처리 시간 변동, 자원 경쟁, 여러 ref를 건드리는 한 요청 때문에 표와 달라진다. 특히 요청 하나의 실행 시간이 그대로여도 줄을 서는 시간이 줄어 전체 응답이 빨라질 수 있다. 이 둘을 구분해 관측하는 방법은 지연 시간과 임계 경로에서 이어서 볼 수 있다.
로컬 Git에서 확인한 거절과 복구
2026년 10월 28일 macOS arm64, Git 2.48.1, Python 3.11.12에서 자체 제작한 네 사례를 실행했다. 개인 Git 설정·원격 서버를 사용하지 않고 임시 bare 저장소에 빈 트리를 공유하는 커밋 세 개를 만들었다. 커밋 이름은 아래 설명에서 base, alice, bob으로 줄였다.
검증용 Python 파일을 내려받아 python3 git-ref-state-transitions.py로 실행할 수 있다. Python 3과 PATH에서 찾을 수 있는 Git이 필요하다. 스크립트가 임시 저장소를 만들고 끝나면 제거하며, 기존 작업 저장소를 수정하지 않는다. 이번 실행 요약에는 버전·상태 전이·가상 계산을 따로 기록했다.
| 사례 | 실행한 조건 | 실제 확인한 최종 상태 |
|---|---|---|
| 같은 ref의 오래된 예상값 | shared를 base→alice로 바꾼 뒤, base라고 예상하며 bob으로 변경 | 두 번째 거절, shared는 alice 유지 |
| 서로 다른 ref | alice 브랜치와 bob 브랜치를 각각 base에서 변경 | 각 브랜치에 자기 결과 보존 |
| 두 ref 묶음에 오래된 예상값 포함 | catalog는 이미 bob인데 base로 예상하며 release와 함께 변경 | 묶음 거절, release=base·catalog=bob 유지 |
| 확인한 현재 값으로 묶음 재작성 | catalog의 예상값을 bob으로 재확인한 예제로 변경 | release와 catalog가 모두 alice |
두 번째 사례도 명령을 순서대로 실행했다. 이 표는 성공·거절 후 저장된 상태를 확인한 결과이며 동시 경주 시험이나 처리량 측정이 아니다. 빈 트리를 사용했으므로 실제 소스의 충돌 해결 능력도 시험하지 않았다.
첫 사례에서 거절은 작업을 버리라는 뜻이 아니다. 바뀐 현재 값을 읽고 상대 변경과 자신의 결과를 비교할 계기다. 실무에서는 병합하거나 새 기준에서 작업을 다시 만들거나 사람이 판단하도록 넘길 수 있다. 예상 OID만 최신 값으로 바꾸고 같은 내용을 덮어쓰면 검사는 통과해도 상대 작업을 잃을 수 있다. 마지막 사례는 그 위험을 해결한 병합기가 아니라, 재검사한 조건에서 묶음 갱신이 가능한지 확인하는 교육용 입력이다.
두 이름을 함께 바꿔야 할 때는 무엇을 보장할까
릴리스 브랜치와 별도 카탈로그 브랜치가 같은 산출물을 가리켜야 한다고 가정하자. 한쪽만 바뀌면 소비자가 서로 다른 버전을 조합할 수 있다. 독립 처리하기 전에 두 값 사이에 지켜야 할 관계가 있는지부터 확인해야 하는 경우다.
Git의 update-ref --stdin은 start, prepare, commit으로 여러 ref 변경을 묶을 수 있다. 예상값과 잠금을 함께 확보하지 못하면 변경을 적용하지 않는다. 다만 공식 문서는 각 ref의 변경이 원자적이어도 동시 독자가 변경 일부를 볼 수 있다고 명시한다. 쓰기의 성공·실패를 묶었다는 사실이 읽는 쪽에 여러 ref의 단일 스냅샷을 약속하지는 않는다. 현재 update-ref 계약
원격 push에는 --atomic이 있으며, 지원되는 서버에서 전부 갱신하거나 오류 시 아무 ref도 갱신하지 않는 조건을 요청한다. 서버가 지원하지 않으면 실패한다. 이 옵션을 사용해 GitHub에 직접 push하는 실험은 하지 않았고, 내부 ref별 quorum이 이 기능을 구현하는 구체적인 방식도 공개 해설만으로 추정하지 않는다. git-push의 atomic 옵션
필자의 설계 제안은 소비자가 반드시 함께 읽어야 할 버전을 한 커밋 안의 manifest로 고정하는 것이다. 예를 들어 앱 커밋 ID와 자료 커밋 ID를 manifest에 적고, 소비자는 manifest를 가리키는 ref를 한 번 해석한 뒤 그 커밋 안의 값을 사용한다. 작업 중 같은 ref를 다시 읽어 새 값과 옛 값을 섞지 않는다. 단, 적힌 객체의 접근 가능성·보존, 산출물의 실제 호환성은 별도로 검사해야 한다. 이 방식은 이번 실험에 구현한 기능이 아니라 여러 이름을 따로 읽는 문제를 줄이기 위한 응용 설계다.
한 행은 안전하게 바뀌어도 여러 행의 규칙이 깨질 수 있는 PostgreSQL 격리 수준과 비교하면 경계를 잡기 쉽다. 두 시스템이 같은 격리 보장을 제공한다는 뜻은 아니다. 공통 질문은 “성공했다고 확인한 단위와 업무가 요구하는 단위가 같은가”다.
읽기 복제본을 늘리는 일과 팀의 다음 선택
GitHub의 다음 설명은 저장과 연산의 분리다. 가상 쓰기 복제본이 Azure Blob Storage의 영속 저장을 연결하고, 별도 읽기 복제본은 그 저장소에서 캐시를 채운다. 초기의 백그라운드 동기화는 모든 읽기에 맞지 않아, 더 넓은 읽기 지원에는 변경분을 읽기 경로에서 맞추는 후속 단계가 필요하다고 밝혔다. 읽기 복제본 수와 최신 ref를 읽는 보장은 따로 봐야 한다. 읽기 확장과 남은 단계
팀이 당장 바꿀 수 있는 것은 호스팅 내부 quorum 설정이 아니라 작업을 나누고 결과를 받는 방식이다. 다음 표는 이 글의 분석을 자체 자동화 점검에 옮긴 것이다. 각 상황의 빈도나 지연은 직접 관측한 뒤 적는다.
| 지금 겪는 상황 | 먼저 남길 증거 | 검토할 변경 |
|---|---|---|
| 독립 작업이 같은 main에 직접 쓰기 | 대상 ref, 작업 시작 기준 OID, 거절 사유 | 작업 브랜치별 결과 보관 후 통합 단계 분리 |
| push 성공 뒤 테스트·승인에서 대기 | 전송·서버 응답·테스트·승인 시각 | 실제 오래 걸리는 단계의 병렬화/분할 검토 |
| 새 버전과 옛 카탈로그가 섞임 | 소비자가 읽은 두 OID와 읽은 시점 | 한 버전 manifest에 조합 고정, 객체 접근 검사 |
| 실패 후 무조건 다시 push | 현재 ref와 예상 OID, 재판단 내용 | 충돌을 분류하고 내용 검토 뒤 제한된 재시도 |
다음 에이전트 작업을 설계할 때 이 표를 저장해 대상 ref와 결과 인수 조건부터 적어 보자. 업무 지침 자체를 다른 에이전트로 옮길 때의 문제는 Agent Skills 이식성에서 이어진다. 지침의 재사용과 저장소 변경의 성공은 각자 확인할 결과가 있다.
- 병렬화의 단위는 에이전트 수보다 서로 독립적으로 바꿀 수 있는 ref의 수와 분포에 가깝다.
- 오래된 예상 OID가 거절됐으면 현재 내용을 다시 판단한다. 최신 OID로 값만 바꿔 재시도하는 것으로 업무 충돌이 해결되지는 않는다.
- 여러 ref의 변경을 함께 성공시키는 조건과 읽는 쪽이 일관된 한 묶음을 보는 조건은 구분한다.
근거와 원자료
자료의 발행 시점과 이번 글에서 참고한 범위를 함께 기록합니다.
- The bridge to agent-scale Git infrastructure ↗GitHub · 2026-10-21
저장소 잠금과 ref별 직렬화, 가상 쓰기/읽기 복제본과 동기화 후속 단계. 내부 설계 해설이며 전체 전환 완료·자체 성능 측정 아님.
- Git Internals — Git References ↗Pro Git / git-scm.com · 상시 갱신
브랜치 ref와 커밋 객체의 이름 관계.
- git-update-ref, version 2.48.0 ↗Git · 2.48.0
로컬2.48.1과 대응하는 예상 old OID 확인 및 stdin transaction 계약. 최신 batch-updates 사용 없음.
- git-update-ref — current manual ↗Git · 2.56.0 문서 목록 / 본문 마지막 변경2.53.0
ref 묶음 변경의 조건과 동시 독자에게 일부 변경이 보일 수 있다는 제한.
- git-push — atomic transactions ↗Git · 2.56.0 문서 목록 / 본문 마지막 변경2.55.0
--atomic 지원 서버에서 전부 또는 없음, 미지원시 실패. 실제 원격 push 미실행.
갱신 기록
첫 원고. 공식 인프라 해설, 자체 가상 대기 계산, Git 2.48.1의 로컬 상태 전이 실험을 구분했다.
오류를 발견했다면 댓글로 알려 주세요 →
이 글에 댓글 남기기
댓글 공개 방식을 확인하고 있습니다. 개인정보나 비밀 값은 적지 마세요.
댓글을 불러오는 중입니다.
내 댓글 확인·수정·삭제
등록 후 안내된 댓글 번호와 비밀번호를 입력하세요. 승인 대기 중인 댓글도 확인할 수 있습니다.