SDD 워크플로우 실전기 3 - git blame으로는 AI 기여도를 셀 수 없었다
git blame으로는 AI 기여도를 셀 수 없었다 — 라인 단위 attribution 파이프라인
1편("기획서와 코드 사이를 잇고…")·2편("잔존률은 높은데, 절반이 뒤집혔다")의 후속입니다.
2편에서 잔존률이라는 지표를 다시 문제 삼았다. 잔존률이 높다고 기획이 안정적이었던 건 아니라고. 그런데 한 발 물러서면 더 앞선 질문이 있다. 그 잔존률은 애초에 어떻게 측정되는가?
1편에서 "1차 MVP는 기록을 안 남겨 측정할 수 없었고, 2차부터 git notes로 기록했다"고 한 문장으로 넘어갔다. 이 글은 그 한 문장의 구현이다. 결론부터 말하면, 이건 분석 문제가 아니라 기록 문제였다.
이 파이프라인의 초기 구현은 팀원이 만든 것을 받아 적용했다. 이 글은 그 구조가 왜 이렇게 생겼는지, 사후 복원이 왜 불가능한지를 정리한 기록이다.
사후에는 복원할 수 없다
처음엔 나중에 git blame으로 세면 될 줄 알았다. AI가 쓴 라인이 최종 코드에 얼마나 남았는지, 커밋 기록만 있으면 역산할 수 있을 것 같았다. 안 된다. 세 가지가 신호를 지운다.
첫째, 사람이 AI의 라인을 손대는 순간 blame은 사람으로 바뀐다. AI가 쓰고 사람이 한 글자 고치면, 그 라인의 최종 저자는 사람이다. "AI가 초안을 쓰고 사람이 보정했다"는 협업의 실제 모습이 blame에는 남지 않는다.
둘째, rebase·squash가 좌표를 뒤튼다. 여러 커밋이 하나로 합쳐지고 라인이 위아래로 밀리면, "몇 번째 줄을 누가 썼나"라는 좌표 자체가 재작성된다.
셋째, 그래서 최종 diff만 봐서는 AI와 사람을 나눌 방법이 없다. diff는 "무엇이 더해졌나"는 알려주지만 "누가 더했나"는 모른다.
믿을 수 있는 신호는 딱 한 순간에만 존재한다. 편집이 일어나는 그 시점. AI 도구가 파일을 고치기 직전과 직후 사이에는 사람이 끼어들지 않는다. 그 순간의 diff는 전부 AI 기여다. 이 시점을 놓치면 신호는 영영 사라진다. 그러니 사후 분석이 아니라 작성 시점에 붙잡아 두는 수밖에 없었다.
작성 시점에 라인 범위를 붙잡는다
첫 단계는 AI 코딩 도구의 편집 훅에 얹혀 있다. 도구가 파일을 편집(생성·수정)할 때마다 훅이 돌면서, 그 편집이 추가한 라인 번호를 계산한다. 이미 추적 중인 파일은 그 순간의 diff에서 추가분만, 새 파일은 전체 라인을 추가분으로 잡는다.
붙잡은 정보는 최소한이다. 파일 경로, 그 파일 내용의 해시, 그리고 연속된 라인을 시작–끝 범위로 압축한 목록. 여기에 "이 편집을 한 주체(AI 세션)"를 붙여 한 줄짜리 기록으로 만들고, 로컬 작업 로그 파일에 계속 덧붙인다. 아직 커밋 전이라, 이 로그는 저장소 바깥의 임시 공간에 쌓인다.
핵심 가정은 앞 절의 시점 불변식 하나다. "편집 직전~직후 사이엔 사람이 없다 → 이 diff의 추가 라인은 AI 기여다." 단순하지만, 사후 복원이 불가능한 상황에서 유일하게 방어 가능한 가정이다. (대가도 있다. 셸을 통한 다중 파일 편집처럼 이 훅을 우회하는 편집은 잡히지 않는다. 이건 뒤에서 한계로 다시 다룬다.)
커밋에 붙여 함께 옮긴다
작업 로그는 로컬 임시 파일일 뿐이다. 이걸 커밋에 묶어 영구 기록으로 만드는 게 두 번째 단계, post-commit 훅이다.
커밋이 끝나면, 직전까지 쌓인 체크포인트를 모아 하나의 attestation(증언) 노트로 접는다. 저장은 커밋 메시지도 코드도 아닌 git notes — 커밋에 사후적으로 메모를 붙이는 git의 별도 저장 공간이다. 코드 히스토리를 건드리지 않고 부가 정보만 따로 얹을 수 있다.
이 단계가 앞서 말한 "좌표 뒤틀림"을 흡수한다.
- 이동한 라인: 커밋으로 라인이 밀렸으면, 내용 해시로 같은 블록을 찾아 attribution을 이월한다. 라인 번호가 바뀌어도 내용이 같으면 따라간다.
- 사라진 라인: 체크포인트 이후 삭제·교체돼 최종 커밋에 없는 라인은 걸러내고, 그 차이를 "덮어써진 라인" 수로 따로 남긴다.
- 파일 이름 변경: rename을 추적해 경로를 옮겨줘야 attribution이 미아가 되지 않는다.
기록을 마치면 로컬 작업 로그는 보관용으로 archive하고 정리한다.
여기서 한 가지가 더 필요하다. git notes는 git push에 자동으로 딸려가지 않는다. 그래서 세 번째 단계, pre-push 훅이 붙는다. push 직전에 원격의 노트를 먼저 당겨와 로컬 기록과 병합하고, 그 다음 노트 ref를 원격으로 밀어 올린다. 병합 충돌은 원격 것을 통째로 지우는 게 아니라 커밋 노트 단위로 로컬 판본을 우선하는 방식이다 — 각 커밋의 attestation은 그 커밋을 작성한 사람이 쓰므로, 서로 다른 사람의 기록이 같은 노트에서 부딪혀 유실될 일은 거의 없다. 동시에 여럿이 밀어 실패하거나 일시적 네트워크 오류가 나면 몇 차례 다시 시도한다.
이 지점이 1차 MVP가 측정 불가였던 진짜 이유이기도 하다. 이 훅과 노트 push 설정이 팀원 각자에게 있어야 데이터가 채워진다. 한 명이라도 빠지면 그 사람의 커밋은 기록 없이 지나가고, 나중에 그 라인은 "누가 썼는지 모름"으로 떨어진다. 측정은 개인의 습관이 아니라 팀의 기본 동작이어야 성립했다.
무엇을 저장하고, 무엇을 저장하지 않는가
노트의 형식은 단순하다. 파일마다 <해시> <라인 범위 목록>이 들어가고, 아래에 JSON 메타데이터가 붙는다. 저자 구분은 해시 규약으로 한다. AI 세션은 그냥 16자리 해시, 사람은 앞에 h_가 붙은 해시. 메타에는 세션 식별자, 도구 사용 집계, 추가·삭제 라인 수처럼 원문을 복원할 수 없는 요약 정보만 들어간다.
여기서 이 시스템에서 가장 중요한 결정을 하나 짚어야 한다. 무엇을 저장하지 않는가.
초기 버전은 편의를 위해 AI와의 대화 transcript 전체를 노트에 그대로 넣었다. 사용자 프롬프트, 모델 응답, 도구 호출 입력이 전부 커밋에 붙어 원격까지 밀려 올라갔다는 뜻이다. 측정을 위해 대화 내용을 조직 저장소에 영구 게시하는 셈이었다. 위험했다.
다음 버전에서 이걸 전부 들어냈다. 프롬프트 원문, 모델 응답, 모든 도구 입력 페이로드는 노트에서 제거된다. transcript는 노트를 만드는 그 순간에만 읽혀서 도구 사용 집계 같은 요약값만 파생하고, 원문은 노트에 남기지 않는다. 요약값을 뽑을 때도 프롬프트 내용이 새어 나가지 못하도록 안전한 형태만 통과시킨다. 대화 원문은 노트에 남기지 않고, 원격 저장소로 push하지 않는다.
정리하면 이렇다. 기여는 라인 단위로 측정하되, 대화 원문은 attribution 노트와 원격 저장소에 남기지 않는다. 측정 가능성과 프라이버시를 맞바꾸지 않으려는 선이었고, 이 시스템에서 가장 신경 쓴 부분이다.
최종 산출물과 대조해 잔존률을 낸다
기록이 쌓였으면 이제 계산이다. 방식은 교집합이다. 노트가 "AI가 썼다"고 주장한 라인 범위와, 산출물별 기준 커밋에 실제로 남은 라인 집합을 겹쳐, 그 겹친 길이를 센다. AI가 쓴 라인이 최종본까지 살아남았는지를 라인 단위로 검증하는 것이다.
계산의 기준 시점을 분명히 해두자. 기록은 편집이 일어난 작성 시점에 남기지만, 잔존 여부는 산출물별 기준 커밋에서 판단한다. 예를 들어 코드 잔존률은 에픽의 최종 코드 머지 커밋을, PRD·디자인 문서 잔존률은 해당 문서의 측정 기준 커밋을 기준으로 다시 대조했다. "살아남았다"는 건 그 기준 커밋에 그 라인이 아직 있다는 뜻이다.
이 대조로 커밋의 추가 라인을 네 갈래로 나눈다.
- AI: AI로 귀속됐고, 이후 수정 없이 최종본에 그대로 남은 라인.
- mixed: AI가 만들었지만 이후 사람의 수정·덮어쓰기를 거쳐 최종본에 남은 라인. "AI 초안을 사람이 보정한" 흔적이 여기 남는다.
- 사람: 사람에게 명시적으로 귀속된 라인.
- unknown: 어떤 노트도 붙지 않은 추가 라인.
분모는 그 커밋이 최종적으로 추가한 전체 라인, 즉 AI + mixed + 사람 + unknown이다. 그리고 이 글(과 1편)에서 말한 잔존률은 (AI + mixed) / 전체 추가 라인으로 계산했다. AI가 그대로 남긴 라인과, AI가 만들었지만 사람이 보정해 남은 라인 — 둘 다 출발점이 AI이므로 AI 기여로 집계한다. 다만 mixed는 따로 떼어 "AI 초안이 얼마나 사람 손을 탔나"를 함께 본다. 순수하게 손대지 않은 AI 비율만 보고 싶으면 mixed를 뺀 값을 따로 계산할 수도 있지만, 공개한 수치는 둘을 합한 기준이다.
unknown이라는 갈래가 중요하다. 노트가 없으면 그 라인은 자동으로 unknown이 된다. 1차 MVP가 "측정 불가"였다는 말의 정체가 바로 이것이다 — 낮은 점수가 아니라, 분류할 근거 자체가 없어 전부 unknown으로 떨어지는 상태. 측정 실패는 곧 unknown이다.
잔존률만으로는 부족해서 채택률을 뒀다
잔존률에는 빈틈이 하나 더 있다. 한 번에 잘 나온 산출물과, 열 번 스무 번 다시 시켜서 겨우 나온 산출물이 같은 잔존률을 받을 수 있다. 최종본에 남은 비율만 보면 둘이 구분되지 않는다.
그래서 잔존률을 시행 횟수로 보정한 채택률을 뒀다. 같은 결과를 적은 시도로 얻을수록 도구가 잘 수렴한 것으로 본다. 시행이 늘수록 완만하게 감점하는 형태다 — 한두 번은 정상 루프라 사실상 감점이 없고, 수십 번을 넘어가면 의미 있는 페널티가 붙는다.
솔직히 말하면 이 감점 곡선의 계수에 엄밀한 이론적 근거는 없다. "반복이 많을수록 깎되, 정상적인 몇 번의 반복은 벌하지 않는다"는 방향성을 담은 휴리스틱이다. 그래서 채택률도 절대 점수가 아니라, 잔존률 옆에 놓고 "이 결과가 몇 번 만에 나왔나"를 같이 읽기 위한 보조 지표로만 쓴다.
남은 한계
이 파이프라인은 정밀 계측기가 아니라 운영 지표를 위한 기록 장치다. 경계가 분명하다.
- 추가 라인만 본다. attribution은 추가된 라인에만 붙는다. AI가 많은 코드를 지워 단순하게 만든 기여는 잔존률에 잡히지 않는다.
- 머지 커밋은 범위 밖이다. 부모가 둘인 커밋은 라인 attribution을 계산하지 않는다.
- 아주 큰 커밋은 건너뛴다. 대량 rebase·squash처럼 변경 규모가 임계치를 넘으면 실시간 계산을 포기한다.
- 훅을 우회한 편집은 못 잡는다. 셸을 통한 다중 파일 편집 등은 기록되지 않아 unknown으로 남는다.
- 표본이 작으면 채택률은 산출하지 않는다. 시행 횟수 정보가 없으면 보정값을 내지 않고 비운다.
이 한계들은 대부분 "측정을 위해 개발 흐름을 방해하지 않는다"는 선택의 결과다. 완벽한 계측을 위해 커밋을 느리게 만들거나 편집마다 무거운 작업을 얹는 대신, 놓치는 부분은 정직하게 unknown으로 두기로 했다.
남은 생각
1편은 "AI를 썼는가"를 "어디에, 어느 정도, 어떤 한계와 함께 남았는가"로 바꾸는 이야기였다. 그 전환이 가능했던 건 똑똑한 사후 분석 덕분이 아니라, 작성 시점에 라인 범위를 붙잡아 커밋에 묶어 옮기고, 그 과정에서 대화 원문 대신 숫자만 남기는 기록 시스템이 있었기 때문이다.
측정은 분석이 아니라 기록에서 시작한다. 그리고 무엇을 기록하지 않을지를 정하는 것이, 무엇을 기록할지를 정하는 것만큼 중요했다.