sgkstudio.
엔지니어링

git 커밋 수가 부풀려진 이유

git 커밋 수로 개발 활동량을 재던 스크립트가 켠 「커밋 +636」 경보는 자동 커밋이 부풀린 숫자였고, 사람이 만든 커밋만 다시 세면 422건이었습니다. 한 저장소는 커밋 979개 중 947개가 스크립트가 남긴 자동 동기화 커밋이었고, 코드 줄 수도 16%가 git이 추적하지 않는 파일에서 나왔습니다. 경보가 옳다는 가설부터 확인해 원인을 좁히고, 자동 커밋을 목록으로 막았다가 세 번째 자동 커밋을 만나 감지기로 바꾼 과정을 적었습니다.

2026-09-13활동량 경보 실측(2026년 9월 11일) — 경보 증가분 636건 vs 사람 커밋 증가분 422건저장소별 커밋 분해 — claude-os 979개 중 자동 동기화 커밋 947개 · threads-loop 523개 중 예약 수집 커밋 165개코드 줄 수 분해 — 전체 1,340,363줄 중 git 추적 밖 210,303줄(16%) · mobile-jarvis 37,939줄 중 추적 제외 업스트림 복제본 37,362줄과거 스냅샷 소급 재계산 — 9월 4일 기준 커밋 6,920개 → 5,438개 · 반복 제목 감지 기준 14일 7건(사람 커밋 최대 2건) · 회귀 테스트 6개 통과

증상 — 활동량 경보가 「커밋 +636」을 띄웠다

2026년 9월 11일, 저장소 활동량을 재는 스크립트가 경보를 켰습니다. 문구는 「커밋 +636」이었습니다. 앞선 스냅샷과 비교해 커밋이 636건 늘었다는 뜻입니다.

이 스크립트는 저장소마다 두 가지를 셉니다. 하나는 커밋 수로, `git rev-list --count HEAD` 결과를 그대로 씁니다. 다른 하나는 코드 줄 수로, 저장소 디렉토리를 처음부터 끝까지 순회하며 파일의 줄 수를 더합니다. 두 값을 날짜별 스냅샷에 남기고, 스냅샷 사이 증가분이 경보의 재료가 됩니다.

숫자만 보면 좋은 소식이었습니다. 커밋이 636건 늘었다면 그만큼 일을 많이 했다는 뜻이니까요. 확인할 일은 하나였습니다. 이 숫자를 그대로 믿어도 괜찮은가.

처음 의심한 것 — 경보가 맞고, 작업이 정말 늘었다

가장 먼저 세운 가설은 경보가 옳다는 쪽이었습니다. 스크립트 자체가 이 가설을 전제로 짜여 있었습니다. 커밋 하나를 사람 작업 하나로, 저장소 폴더 안의 파일을 우리가 쓴 코드로 읽는 구조입니다.

  • 전제 1 — 커밋 수는 사람이 한 작업의 수와 같다 (`git rev-list --count HEAD`)
  • 전제 2 — 폴더 안 코드 줄 수는 우리가 쓴 코드의 양과 같다 (디렉토리 순회)

두 전제가 맞다면 할 일이 없습니다. 경보는 정상이고 숫자는 실적입니다. 반대로 틀렸다면 경보 숫자 안에 사람이 아닌 주체가 만든 커밋이나, 우리가 쓰지 않은 파일이 섞여 있어야 합니다. 커밋과 파일을 만든 주체별로 나눠 세면 어느 쪽인지 드러납니다.

의심할 여지는 처음부터 있었습니다. 이 저장소들에는 사람만 커밋하지 않습니다. 예약 작업이 데이터를 모을 때마다 커밋을 남기고, 동기화 스크립트도 스스로 커밋합니다. 다만 그 몫이 경보 숫자에서 얼마나 차지하는지는 세어 본 적이 없었습니다.

커밋 수는 무엇을 세고 있었나

확인 수단은 단순했습니다. 저장소별로 커밋 목록을 뽑아 제목으로 묶었습니다. 사람이 쓴 커밋은 제목이 제각각이고, 스크립트가 남기는 커밋은 날짜나 번호만 바뀐 같은 모양으로 반복합니다. 제목만 봐도 주체가 갈립니다.

파일 쪽은 `git ls-files` 가 돌려주는 목록, 즉 git이 실제로 추적하는 파일과 디렉토리 순회 결과를 맞대 보았습니다. 순회에는 잡히지만 추적 목록에는 없는 파일이 있다면, 그 줄 수는 저장소 코드가 아니라 디스크에 놓여 있던 다른 무언가입니다.

명령 자체도 다시 읽었습니다. `git rev-list --count HEAD` 는 현재 브랜치에서 닿는 커밋을 전부 셉니다. 누가 만들었는지, 병합 커밋인지는 묻지 않습니다. 이 명령이 돌려준 숫자는 처음부터 「사람 작업 수」가 아니라 「커밋 개수」였습니다. 두 말이 같다고 가정한 쪽은 명령이 아니라 우리였습니다.

알리바이 — 사람 커밋은 422건뿐이었다

나눠 세자 첫 가설은 바로 무너졌습니다. 경보가 말한 증가분 636건 가운데 사람이 만든 커밋은 422건이었습니다. 나머지는 사람 커밋으로 칠 수 없는 커밋, 곧 자동 커밋과 병합 커밋이었습니다.

경보가 말한 커밋 증가분 636건과 사람 커밋만 센 증가분 422건을 비교한 막대그래프
경보 숫자와 사람 커밋 숫자가 갈렸다

저장소별로 보면 차이가 더 선명했습니다.

저장소별 전체 커밋과 자동 커밋 (2026년 9월 11일)
저장소전체 커밋자동 커밋자동 커밋 제목 모양
claude-os979개947개sync 2026-09-11 (SGK) — 자동 동기화
threads-loop523개165개cron: 20260911_… — 예약 수집 작업

제목 모양은 스크립트가 남기는 커밋의 실제 제목 형식입니다. 날짜 부분만 매번 바뀝니다.

저장소별 전체 커밋과 자동 커밋 막대그래프. claude-os 전체 979개 중 자동 947개, threads-loop 전체 523개 중 자동 165개
한 저장소는 커밋 대부분이 자동 동기화였다

`claude-os` 는 커밋 979개 중 947개가 동기화 스크립트가 날짜만 바꾼 같은 제목으로 남긴 커밋이었습니다. 사람이 손으로 만든 커밋은 그 나머지뿐입니다. `threads-loop` 는 523개 중 165개가 예약 수집 작업의 커밋이었습니다.

전제 1은 여기서 깨졌습니다. 커밋 수는 사람 작업을 세지 않았고, 경보 숫자의 상당 부분은 사람 손을 거치지 않은 커밋이었습니다.

진짜 원인 — 커밋 수는 누가 만들었는지 묻지 않는다

원인은 계측 방식 한 줄에 있었습니다. 커밋 개수를 그대로 활동량으로 쓰면, 자동화가 커밋을 하나 남길 때마다 활동량도 하나 오릅니다. 예약 작업을 늘릴수록 경보가 더 쉽게 켜지는 구조였습니다.

자동 커밋은 사람 커밋보다 규칙적이기도 합니다. 사람은 일이 있는 날 커밋하지만, 스크립트는 정해진 주기에 맞춰 커밋합니다. 뒤에서 세 번째로 찾은 자동 커밋은 14일 동안 14건이 쌓여 있었습니다. 이런 커밋이 섞인 증가분은 사람이 쉬는 동안에도 오르고, 사람 작업의 오르내림을 가립니다.

병합 커밋도 같은 방향으로 작용합니다. `--count HEAD` 는 브랜치를 합칠 때 생기는 커밋까지 셉니다. 병합 커밋은 새 작업이 아니라 이미 한 작업을 합친 기록이라, 활동량에 더하면 같은 일을 두 번 세는 쪽에 가깝습니다.

코드 줄 수는 어디서 부풀었나

전제 2도 같은 방법으로 확인했습니다. 디렉토리 순회로 센 코드는 1,340,363줄이었는데, 그중 210,303줄(16%)이 git이 추적하지 않는 파일이었습니다.

가장 큰 덩어리는 한 저장소 안에 있었습니다. `mobile-jarvis` 는 37,939줄 중 37,362줄이, 외부에서 받아 온 원본 저장소(업스트림)를 통째로 복제해 두고 git 추적에서 뺀 폴더에서 나왔습니다. 우리가 한 줄도 쓰지 않은 코드가 그 저장소 줄 수의 거의 전부를 차지했습니다.

디렉토리 순회는 디스크에 놓인 파일을 가리지 않고 셉니다. 복제해 둔 외부 코드든, git에서 일부러 뺀 파일이든 구분하지 않습니다. 커밋 수와 마찬가지로, 코드 줄 수도 「누구의 코드인가」를 묻지 않는 방식이었습니다.

고친 방법 — 사람 커밋과 추적 파일만 센다

고친 방법은 두 전제를 명령 수준에서 바로잡는 일이었습니다.

  • 커밋 수 — 병합 커밋을 빼는 `--no-merges` 를 붙이고, 알려진 자동 커밋 제목을 `AUTO_COMMIT_RE` 정규식으로 걸러 사람 커밋만 셉니다. 처음 등록한 모양은 `sync 2026-09-11 (SGK)` 와 `cron: 20260911_…` 두 종류였습니다.
  • 코드 줄 수 — 디렉토리 순회 대신 `git ls-files` 가 돌려주는 추적 파일만 셉니다.
계측 방식 비교. 바꾸기 전에는 git rev-list --count HEAD 와 디렉토리 전체 순회, 바꾼 뒤에는 --no-merges 와 --until 로 뽑은 커밋에서 AUTO_COMMIT_RE 제목을 빼고, 코드 줄 수는 git ls-files 파일만 합산
세는 대상을 사람 커밋과 추적 파일로 좁혔다

과거 스냅샷도 다시 계산했습니다. 커밋은 git 이력이 시점을 기억하므로, 스냅샷을 찍은 시각을 `--until=<captured_at>` 으로 넘기면 그때 기준 사람 커밋 수를 되살릴 수 있습니다. 9월 4일 스냅샷의 커밋 수는 6,920개에서 5,438개로 내려왔습니다. 재계산 전에는 기존 스냅샷 전체를 압축해 백업해 두었습니다.

다시 센 결과 세 가지. 9월 4일 스냅샷 커밋 수 6,920개에서 5,438개로 재계산, 코드 1,340,363줄 중 210,303줄(16%)이 git 추적 밖, mobile-jarvis 37,939줄 중 37,362줄이 추적 제외 업스트림 복제본
다시 세자 커밋도 줄 수도 내려앉았다

코드 줄 수는 같은 방식으로 되돌릴 수 없었습니다. git은 추적하지 않는 파일의 과거 상태를 남기지 않으므로(git의 기본 동작), 그 시점 디스크에 무엇이 있었는지 다시 알 길이 없습니다. 그래서 스냅샷마다 줄 수를 어떤 방식으로 셌는지 `loc_method` 로 기록하고, 방식이 다른 스냅샷끼리는 줄 수를 비교하지 않게 했습니다.

알려진 자동 커밋 두 종류만 막으면 끝나는가

여기까지 고친 뒤 남은 가설은 이랬습니다. 자동 커밋은 동기화와 수집 두 종류이니, 이 둘을 정규식에 올리면 계측은 깨끗해진다.

이 가설은 두 종류를 막은 직후 깨졌습니다. 세 번째 자동 커밋이 나왔습니다. 제목은 `chore(data): 파이프라인 상태물 일일 정산` 이었고, 14일 동안 14건이 쌓여 있었습니다. 데이터 파일을 매일 정리하는 작업이 남기는 커밋으로, 먼저 등록한 두 모양 어디에도 맞지 않았습니다.

목록은 우리가 이미 아는 자동화만 막습니다. 자동화는 계속 새로 생기고, 새로 생긴 자동화는 목록에 오르기 전까지 사람 커밋 행세를 합니다. 목록을 늘리는 방식으로는 다음 자동화가 나올 때마다 같은 사고를 한 번씩 겪습니다.

같은 실수를 막는 장치 — 목록 대신 반복 제목 감지기

그래서 재발 방지를 목록이 아니라 감지기로 옮겼습니다. `_auto_suspects()` 는 최근 14일치 커밋 제목에서 숫자를 지운 뒤 같은 제목끼리 묶습니다. 날짜와 번호만 바뀌는 자동 커밋은 숫자를 지우면 한 제목으로 모입니다.

같은 제목이 14일 동안 7건 이상이면 그 제목을 스냅샷의 `auto_suspects` 항목에 올리고, 경보를 주황색으로 바꿔 자동 커밋 목록에 추가하라고 요구합니다. 감지기는 커밋을 스스로 빼지 않고, 사람에게 알리는 데서 멈춥니다.

반복 제목 감지 흐름도. 최근 14일 커밋 제목에서 숫자를 지우고, 같은 제목이 7건 이상이면 스냅샷 auto_suspects 에 기록한 뒤 경보를 주황색으로 바꿔 자동 커밋 목록 추가를 요청하고, 7건 미만이면 기존 규칙대로 집계
새 자동화는 목록에 오르기 전에 감지기에 먼저 걸린다

기준을 7건으로 잡은 근거는 사람 커밋 실측입니다. 사람이 만든 커밋은 같은 제목이 14일 동안 2건을 넘은 적이 없었습니다. 7건이면 사람 커밋의 최대치와 거리가 넉넉하고, 하루 한 번 도는 작업은 일주일이면 기준에 닿습니다.

회귀 테스트 6개도 함께 두었고 전부 통과합니다. 세 번째 자동 커밋 같은 사례가 다시 나와도, 이번에는 경보가 먼저 알려 줍니다.

커밋 수만의 문제였나 — 감시 스크립트가 남의 기록을 제 것으로 센 사례

비슷한 모양의 오류는 다른 감시 스크립트에서도 나왔습니다. 저장소 동기화 로그를 읽는 스크립트가 「최근 24시간 47사이클 중 22회 실패」를 하루 종일 띄웠는데, 실제로 실패한 사이클은 1건이었습니다.

이 스크립트는 로그를 사이클 시작 표시로 잘라 블록을 만들고, 블록 안에 실패 기호(⛔)가 있으면 그 사이클을 실패로 셌습니다. 그런데 로그의 ⛔ 79줄 가운데 자기 사이클 표시를 단 줄은 0줄이었습니다.

  • 50줄 — 동기화와 무관한 다른 검사 도구가 같은 로그 파일에 찍은 경고
  • 19줄과 10줄 — 동시 실행을 막는 잠금(락) 경쟁에서 진 쪽이 남긴 건너뜀 기록. 자기 표시 없이 한 줄만 남기고 끝나, 직전에 시작한 다른 역할의 블록에 섞였습니다

아침 한 번의 잠금 경쟁이 18회 실패로 부풀었고, 그 뒤 13시간 동안 정상 완주만 이어졌는데도 빨간불이 꺼지지 않았습니다. 지금 세는 역할의 태그가 붙은 ⛔만 그 역할의 실패로 세도록 고치자, 내려받기 쪽은 4회에서 1회로, 올리기 쪽은 18회에서 0회로 내려왔습니다.

두 사례의 공통 구조
계측무엇을 셌나묻지 않은 질문부푼 숫자
활동량 커밋 수현재 브랜치의 커밋 전부이 커밋은 누가 만들었나636건 → 사람 커밋 422건
동기화 실패 수블록 안의 ⛔ 전부이 줄은 누가 썼나22회 → 실제 1건

두 사례는 모양이 같습니다. 커밋 수는 커밋을 누가 만들었는지 몰랐고, 로그 블록 분할은 그 줄을 누가 썼는지 몰랐습니다. 여러 주체가 한곳에 기록을 쌓는 순간, 「이 기록이 누구 것인가」는 계측이 먼저 답해야 할 질문입니다. 이 질문을 건너뛴 집계는 남의 몫을 내 실적이나 내 실패로 셉니다.

남은 한계

코드 줄 수의 과거 비교는 여전히 못 합니다. 방식을 바꾸기 전 스냅샷과 뒤 스냅샷은 `loc_method` 가 달라서, 줄 수 추이가 그 지점에서 끊깁니다.

감지기는 제목만 봅니다. 숫자를 지워도 제목이 매번 달라지는 자동 커밋은 잡지 못하고, 사람이 같은 제목으로 짧은 기간에 여러 번 커밋하는 방식으로 일한다면 7건 기준도 다시 봐야 합니다. 지금은 이 둘을 한계로 적어 둡니다.

git 커밋 수로 팀이나 저장소의 활동을 재고 있다면, 숫자를 보기 전에 그 저장소에 커밋하는 주체가 몇인지부터 세어 보길 권합니다. 커밋 수로 개발 활동을 재 본 앞선 기록은 개발자 생산성 측정, 커밋 수만 늘어서는 안 보였다에, 감시 스크립트가 실패를 정상으로 읽은 사례는 크론 실패 감지 — 감시기가 4일 내내 정상이라고 말했다에 있습니다. 자동화가 남기는 기록과 사람 작업을 가르는 계측이 필요하다면 상담에서 이야기 나눌 수 있습니다.

같은 방식으로 우리 업무를 재보고 싶다면

이 글에 쓴 계측·판정·자동화는 저희가 실제로 매일 돌리는 방식 그대로입니다. 어떤 작업을 자동화할 수 있고 어디를 사람이 잡아야 하는지, 현재 업무 흐름을 놓고 같이 짚어 드립니다.

새 글이 올라오면 알려 드립니다

이런 계측 기록을 새로 쓰면 메일로 한 통 보냅니다. 광고나 판촉은 보내지 않습니다.