두 달 전에 망이 분리된 환경에서 커밋을 옮기는 절차를 자동화했습니다.
개발은 외부망에서 하고 정본은 내부망에 두는데, 두 망 사이엔 push/fetch가 안 되고 클립보드 텍스트만
건너갑니다. 그래서 format-patch 로 패치를 만들어 클립보드에 담고, 반대편에서 git am 으로 붙이는
절차를 스킬로 굳혔습니다.
그 뒤로는 별생각 없이 썼습니다. "vdi 커밋" 한 마디면 알아서 범위를 잡아주니까요.
어제 그 도구가 16.6MB 패치를 만들어줬습니다.
크기가 이상한데 아무도 말을 안 했다
26만 줄, 730파일. 클립보드에 이만한 게 정말 들어가나 싶어서 확인해봤습니다.
$ pbpaste | wc -lc
266220 16611199
온전히 들어가 있었습니다. 시작 줄도 From 30e66cea... 로 정상이고, 끝도 잘리지 않았습니다.
지금 생각하면 그게 더 문제였습니다. 크기가 평소와 딴판인데 도구는 아무 경고도 하지 않았고, 저는 이걸 그대로 "반대편에서 이렇게 실행하세요"라고 안내하려던 중이었습니다. 패치 자체는 문법적으로 완벽했고, 클립보드도 멀쩡했으니 실패 신호가 어디에도 없었습니다.
"어디까지 보냈는지"를 저장소에 적어뒀다
이 도구의 핵심은 기준점 기억입니다. 매번 마지막으로 보낸 커밋 해시를 파일에 적어두고, 다음엔 그 다음 커밋부터 잡습니다.
- 2026-08-06 10:01:36 | b83b7e3b47af... | 3커밋
- 2026-08-05 14:39:55 | a4b1d9962e57... | 2커밋
어제 읽힌 기준은 b83b7e3b4, 8월 6일 것이었습니다. 그런데 실제 마지막 반영은 8월 20일이었습니다.
파일이 거짓말을 한 게 아니고, 제가 다른 파일을 읽었습니다.
# 제가 서 있던 작업 브랜치
$ git show develop-feature:<그 파일> | head -1
- 2026-08-06 ... b83b7e3b47af...
# 통합 브랜치의 같은 파일
$ git show develop:<그 파일> | head -1
- 2026-08-20 ... 5d8ebf3d251e...
기준점을 저장소 안 파일에 저장했으니 브랜치를 따라갑니다. 동료가 통합 브랜치에서 반영하고 기록을 남기면 그 기록은 통합 브랜치에만 생기고, 제 작업 브랜치에는 2주 전 것이 그대로 남습니다.
그리고 저는 작업 브랜치에 서 있었습니다. 도구의 타깃 브랜치 기본값이 "현재 체크아웃된 브랜치"였거든요. 만들 때는 그게 편해 보였습니다. 어차피 작업하던 브랜치에서 돌릴 거라고 생각했으니까요. 실제로는 아침에 머지하고 브랜치를 안 바꾼 채로 하루를 보내는 일이 흔했습니다.
기준을 바로잡고 다시 만들어보니 이랬습니다.
| 잘못된 기준(8/06) | 올바른 기준(8/20) | |
|---|---|---|
| 기간 | 15일치 | 하루치 |
| 커밋 | 166개 | 19개 |
| 패치 | 16.6MB · 730파일 | 344KB · 94파일 |
| 처리 방식 | 커밋 1개로 뭉침 | 19개 개별 보존 |
마지막 줄이 제일 아팠습니다. 기준을 잘못 잡으면 크기만 커지는 게 아니라 처리 방식까지 바뀝니다. 잘못된 15일 범위 안에는 손으로 충돌을 해결한 머지가 하나 섞여 있었습니다. 그런 게 있으면 개별 커밋으로 쪼개 보낼 수가 없어서(반대편에서 충돌이 재발합니다) 절차가 "전부 커밋 하나로 뭉치기"로 분기합니다. 그대로 갔으면 커밋 19개의 이력이 통째로 사라진 채 반대편에 "외부망 작업분 통합 반영" 한 줄만 남았을 겁니다.
고쳐서 만들었는데, 30분 늦었다
기준을 바로잡고 19커밋 패치를 만들고, 반대편에서 실행할 명령까지 다 써놨습니다. 그러다 다른 걸 확인하려고 원격을 당겼습니다.
$ git fetch && git log --oneline develop..origin/develop
967a6e85b chore(vdi-commit): VDI 반영 기록 추가 — c45bf2fc7 (21커밋)
동료가 30분 전에 같은 기준점에서 21커밋으로 이미 반영해둔 상태였습니다.
제 19커밋이 그 안에 들어 있는지는 한 줄로 확인됩니다.
$ git merge-base --is-ancestor 2011e0c25 c45bf2fc7 && echo "포함됨"
포함됨
제 패치는 처음부터 만들 필요가 없었습니다.
두 번째는 원인이 달랐다
첫 번째가 "잘못된 파일을 읽었다"였다면 두 번째는 그게 아닙니다. 파일은 맞게 읽었고, 기준점도 맞았습니다. 문제는 두 사람이 같은 기준점에서 동시에 출발한 것입니다.
절차를 다시 보니 구멍이 눈에 보였습니다.
① 기록 파일에서 기준 읽기 ← 로컬 파일만 본다
② 패치 만들기
③ 기록 파일에 새 기준 쓰기
④ 그 기록을 커밋 ← 여기까지. push 는 "필요할 때"
③④를 하고 push를 안 하면, 다음 사람의 ①이 이 회차를 못 봅니다. 그 사람은 같은 기준에서 같은 범위를 또 만듭니다. 저는 이걸 "감지가 없는 문제"라고 생각했는데, 뿌리는 예방이 없는 것이었습니다. 기록이 원격에 없으면 아무리 열심히 봐도 볼 게 없으니까요.
넣은 것 세 가지
두 사고의 공통점은 하나뿐이었습니다. 확인할 수 있는 사실을 확인하지 않았습니다. 원격 기록도 볼 수 있었고, 범위 크기도 잴 수 있었습니다. 어느 쪽도 어렵지 않았고, 그냥 절차에 없었습니다.
첫째, 기준을 계산하기 전에 원격과 대조합니다. 체크아웃 없이 원격 파일을 읽을 수 있어서 비용이 거의 없습니다.
LOCAL=$(grep -oE '[0-9a-f]{40}' "$HIST" | head -1)
REMOTE=$(git show <리모트>/<브랜치>:"$HIST" | grep -oE '[0-9a-f]{40}' | head -1)
원격이 더 최신이면 남이 먼저 반영한 것이니 그 값을 씁니다. 로컬이 더 최신이면 제 기록이 push 안 된 상태라는 뜻이니 push를 안내합니다. 이어서 이번 범위가 이미 반영됐는지 확인하고, 걸리면 패치를 만들지 않고 멈춥니다.
git merge-base --is-ancestor <end> "$REMOTE" && echo "이미 포함됨 — 중단"
둘째, 범위가 이상하게 크면 만들기 전에 물어봅니다. 기준을 잘못 잡으면 원인은 안 보이지만 결과는 비정상적으로 커집니다. 커밋 40개 초과 / 기준이 7일 이상 전 / 패치 5MB 초과 — 세 신호 중 하나라도 걸리면 만들지 말고 사람에게 확인합니다.
셋째, 기록을 push하라고 반드시 안내합니다. 첫째가 감지라면 이건 예방이고, 하나만 넣으면 반쪽입니다.
진짜 걸리는지 돌려봤다
방어를 넣고 "됐다"고 끝내면 걸리는지 알 수 없습니다. 어제의 실제 해시로 세 개를 다 돌려봤습니다.
원격 대조 1단 로컬 8/06 vs 원격 8/21 → 기준 교체 사고 ① 차단
원격 대조 2단 내 끝점이 이미 포함 → 시작 전 중단 사고 ② 차단
범위 이상 감지 잘못된 기준이면 166커밋 → 임계 40 초과 발동
특히 2단이 마음에 들었습니다. 두 번째 사고를 패치를 만들기도 전에 막습니다.
설계상 남는 구멍이 하나 있습니다. A가 패치를 만들고 기록을 push하기 전 몇 분 사이에 B가 시작하면 여전히 겹칩니다. 완전히 막으려면 원격 락이 필요한데 이 규모에 그건 과합니다. 셋째가 그 창을 분 단위로 좁히고, 첫째가 사후에 잡는 정도로 두기로 했습니다.
이건 어디서든 쓸 만합니다
merge-base --is-ancestor — "이 커밋이 저기 이미 들어갔나"를 한 줄로 답합니다. 종료 코드만 보면 되니
스크립트에 넣기 좋습니다. 저는 이걸 몰라서 그동안 git log | grep 같은 걸 했습니다.
git merge-base --is-ancestor <이거> <저기> && echo "포함됨"
git show <remote>/<branch>:<path> — 체크아웃 없이 다른 브랜치·원격의 파일 내용을 읽습니다.
브랜치별로 갈리는 파일을 비교할 때 이만한 게 없습니다. stash 하고 checkout 하고 다시 돌아오는 짓을
안 해도 됩니다.
남은 생각
손으로 하면 16.6MB에서 손이 멈춥니다. 자동화는 안 멈춥니다. 이건 알고 있었는데, 실제로 겪어보니 좀 다르게 느껴졌습니다. 문제는 자동화가 실수를 반복한다는 것보다 결과가 그럴듯하게 나온다는 쪽이었습니다. 패치는 완벽한 형식이었고 클립보드도 온전했습니다. 뭔가 틀렸다는 신호가 결과물 어디에도 없었습니다.
그래서 원인을 못 잡는 자리에서는 결과의 크기라도 재는 게 값이 있다고 생각하게 됐습니다. 무엇이 틀렸는지 몰라도 "이건 평소와 다르다"는 건 알 수 있으니까요. 임계값 세 개는 그래서 넣었습니다.
그리고 상태를 어디에 두느냐 — 이게 두 사고를 관통했습니다. 진행 상태를 버전 관리되는 파일에 넣으면 브랜치가 갈릴 때 상태도 갈립니다. 편하다고 데이터 옆에 메타데이터를 두면 데이터가 갈라질 때 같이 갈라집니다. 공동 주방에서 "우유 샀음"을 냉장고가 아니라 각자 사물함에 붙인 것과 비슷합니다. 두 달 동안은 저 혼자 써서 괜찮았고, 사람이 둘이 되는 순간 락 없는 분산 상태가 됐습니다.
붙이기 전에 둘 다 잡았으니 실제로 잘못 적용된 건 없습니다. 다만 두 번 다 우연히 잡았습니다 — 크기가 이상해서 한 번, 다른 일로 원격을 당겨봐서 한 번. 우연을 절차로 바꾸는 게 어제 한 일의 전부입니다.