본문으로 건너뛰기

어제 만든 패치가 16.6MB였습니다. 정상은 344KB였고요

2026년 8월 21일
3 views
Git
Developer Tools
Claude Code
주니어 개발
회고

두 달 전에 망이 분리된 환경에서 커밋을 옮기는 절차를 자동화했습니다. 개발은 외부망에서 하고 정본은 내부망에 두는데, 두 망 사이엔 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에서 손이 멈춥니다. 자동화는 안 멈춥니다. 이건 알고 있었는데, 실제로 겪어보니 좀 다르게 느껴졌습니다. 문제는 자동화가 실수를 반복한다는 것보다 결과가 그럴듯하게 나온다는 쪽이었습니다. 패치는 완벽한 형식이었고 클립보드도 온전했습니다. 뭔가 틀렸다는 신호가 결과물 어디에도 없었습니다.

그래서 원인을 못 잡는 자리에서는 결과의 크기라도 재는 게 값이 있다고 생각하게 됐습니다. 무엇이 틀렸는지 몰라도 "이건 평소와 다르다"는 건 알 수 있으니까요. 임계값 세 개는 그래서 넣었습니다.

그리고 상태를 어디에 두느냐 — 이게 두 사고를 관통했습니다. 진행 상태를 버전 관리되는 파일에 넣으면 브랜치가 갈릴 때 상태도 갈립니다. 편하다고 데이터 옆에 메타데이터를 두면 데이터가 갈라질 때 같이 갈라집니다. 공동 주방에서 "우유 샀음"을 냉장고가 아니라 각자 사물함에 붙인 것과 비슷합니다. 두 달 동안은 저 혼자 써서 괜찮았고, 사람이 둘이 되는 순간 락 없는 분산 상태가 됐습니다.

붙이기 전에 둘 다 잡았으니 실제로 잘못 적용된 건 없습니다. 다만 두 번 다 우연히 잡았습니다 — 크기가 이상해서 한 번, 다른 일로 원격을 당겨봐서 한 번. 우연을 절차로 바꾸는 게 어제 한 일의 전부입니다.