AI 에이전트의 작업을 관측 가능하게 만들기

AI 에이전트의 작업을 관측 가능하게 만들기

1. 서론: 커밋 로그가 설명하지 못하게 된 것

AI 코딩 도구를 팀에 들이고 나면 코드는 빨리 늘어나는데 그 코드가 어떤 과정을 거쳐 나왔는지는 오히려 흐려집니다. 예전에는 커밋 로그만 훑어도 누가 무엇을 얼마나 붙들고 있었는지 대강 읽혔습니다. 지금은 같은 로그를 봐도 사람이 한 시간 고민한 결과인지 에이전트가 삼 분 만에 만든 것인지 구분되지 않습니다. 저장소는 그대로인데 그 안에서 벌어지는 일이 안 보이게 된 것입니다.

목표는 두 가지로 잡았습니다. 에이전트가 만든 코드라도 나중에 합류한 사람이 저장소만 받고 바로 파악해서 이어받을 수 있어야 한다는 것, 그리고 저장소가 어느 프로젝트의 것이든 분석에 등록되는 순간 별도 설정 없이 지표와 추이가 나와야 한다는 것입니다. 이후의 모든 선택은 이 둘이 결정했습니다.

2. 무엇을 셀 것인가

2.1 판별이 아니라 신고로

처음에는 코드 변경 내용을 분석해 AI가 쓴 부분을 가려내려 했지만 곧 접었습니다. 커밋에는 작성자와 시각과 변경 내용과 메시지가 있을 뿐, “이 줄이 어떻게 만들어졌는가”를 기록하는 자리가 없습니다. AI가 초안을 만들고 사람이 다듬어 올린 코드는 손으로 친 코드와 바이트 단위로 동일합니다. 판별 정확도가 낮은 것이 아니라 판별할 대상이 데이터에 없습니다.

그래서 판별을 포기하고 신고를 받기로 했습니다. 도구가 남기거나 사람이 적은 것만 셉니다. 대신 지표 이름을 “AI 사용률”이 아니라 “신고된 AI”로 못 박았습니다. 신고를 안 하면 어떡하냐는 질문이 따라오는데, 이것은 결함이 아니라 또 하나의 지표로 다루기로 했습니다. 신고율이 낮다는 사실 자체가 규약이 안 지켜지고 있다는 신호이기 때문입니다.

2.2 단위를 커밋이 아니라 세션으로

더 중요한 결정은 무엇을 하나의 작업으로 볼 것인가였습니다. 에이전트와의 작업은 한 번에 끝나지 않습니다. 지시하고, 결과를 보고, 고쳐 달라고 하고, 다시 받습니다. 이 과정이 커밋 서너 개로 쪼개져 남습니다.

커밋을 단위로 세면 “에이전트가 커밋 세 개를 했다”는 사실만 남습니다. 정작 알고 싶은 것은 “이 작업이 몇 번 만에 끝났는가”입니다. 한 번에 끝난 작업과 다섯 번을 되돌린 작업은 같은 세 커밋이어도 전혀 다른 이야기입니다. 그래서 세션 식별자를 커밋에 함께 남기고 같은 세션의 커밋을 하나의 작업으로 묶기로 했습니다.

보는 단위

답할 수 있는 질문

커밋

에이전트가 참여한 커밋이 몇 건인가

세션

한 작업이 몇 번 만에 끝났는가 / 어떤 작업에서 되돌림이 많은가

3. 채택한 방법

3.1 커밋 메시지를 인터페이스로 쓴다

사람이 짠 코드는 이해가 안 되면 물어보면 됩니다. 에이전트가 짠 코드는 그럴 수가 없습니다. 세션이 끝나면 맥락은 사라지고, 다음에 같은 도구를 불러도 지난번에 왜 그렇게 했는지 답하지 못합니다. 그러니 에이전트가 참여한 코드일수록 설명이 코드와 같은 자리에 남아 있어야 합니다. 나중에 합류한 사람이 저장소만 받고도 파악하려면 그 방법밖에 없습니다.

기록을 별도 저장소에 쌓거나 도구 API를 긁어 오는 방법도 있었지만 커밋 메시지를 골랐습니다. 어떤 도구를 쓰든 커밋은 남으니 도구를 가리지 않고, 기록이 코드와 함께 이동해 복제나 미러링에도 따라오며, 원본이 그대로 보존되어 나중에 해석 규칙을 바꿔도 다시 읽으면 되기 때문입니다. 형식은 Git이 이미 갖고 있는 트레일러 관례를 그대로 썼습니다.

[SPEC-008] feat(member): 세션에서 사용자 신원을 읽도록 변경

X-Agent: dev
X-Agent-Session: 3ae7dbc4
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

여기에 함정이 하나 있었습니다. Co-authored-by는 원래 사람끼리의 공동 작업을 표기하는 규격이라, 그대로 쓰면 두 사람이 페어로 작업한 커밋까지 AI 커밋으로 잡힙니다. 널리 쓰이는 규격을 다른 용도로 재사용할 때 흔히 생기는 문제인데, 알려진 도구의 이메일 도메인일 때만 에이전트로 인정하는 방식으로 갈랐습니다.

3.2 규약을 사람이 아니라 도구에 심는다

여기가 가장 예상 밖이었던 부분입니다. 커밋 규약은 원래 잘 안 지켜집니다. 문서로 정하고 온보딩에서 설명해도 몇 주 지나면 흐려집니다. 사람의 성실성에 기대는 규칙이 대개 그렇습니다. 그런데 에이전트는 프로젝트 규약 파일을 매번 읽고 그대로 따릅니다. 기억하는 것이 아니라 작업할 때마다 다시 읽기 때문에 시간이 지나도 흐려지지 않습니다.

## 커밋 메시지 규약

[<백로그>] <type>(<scope>): <제목>

X-Agent: <역할>              # dev · review · test · fix · chore 중 하나
X-Agent-Session: <식별자>    # 세션 내내 같은 값을 쓴다

- 제목 한 줄이면 끝이다. 본문을 쓰지 않는다.
- 한 커밋에 역할 두 개를 담지 않는다.

커밋 규약이라는 개념 자체는 새로운 것이 아닙니다. 다들 중요하다고 알면서도 안 지켜졌을 뿐입니다. 달라진 것은 규약이 아니라 그것을 집행하는 주체입니다. 그래서 규약 파일을 어디에 두느냐가 중요해집니다. 저장소 안에 두면 규약을 고치는 일이 코드 변경과 똑같이 리뷰를 거치고 버전으로 남고, 코드가 바뀌었는데 규약이 그대로면 같은 커밋에서 눈에 띕니다. 위키에 있었다면 코드와 어긋나도 아무도 모른 채 지나갔을 것입니다.

다만 반드시 따라와야 하는 규칙이 하나 있습니다. 사람이 직접 하는 커밋에는 신고 트레일러를 붙이지 않는 것입니다. 편의상 붙이기 시작하면 사람 작업과 에이전트 작업이 섞이고, 그 순간 이 데이터로 답하려던 질문이 통째로 무의미해집니다. 규약을 만들 때는 무엇을 적을지만큼 무엇을 적지 않을지도 정해야 합니다.

역할을 다섯 가지로 제한하고 한 커밋에 하나만 적게 한 것도 같은 맥락입니다. 두 개를 적고 싶어졌다면 커밋을 나눠야 한다는 신호입니다. 목록에 없는 값이 들어와도 버리지 않고 그대로 셉니다. 규약이 지켜지지 않는다는 사실 자체가 봐야 할 신호이고, 그 비율이 곧 규약 준수율 지표가 되기 때문입니다.

3.3 판정은 저장하지 않고 파생시킨다

구현을 마치고 화면을 열었더니 숫자가 예상보다 훨씬 작았습니다. 원인은 데이터가 아니라 설계였습니다. 커밋 엔티티는 불변으로 설계되어 한번 적재되면 값을 바꾸지 않는데, 판정 결과를 그 엔티티의 필드에 저장하고 있었습니다. 파서를 붙이기 전에 들어온 커밋은 그 필드가 영원히 비어 있었던 것입니다.

방법

내용

문제

마이그레이션 배치

과거 행을 훑어 필드를 채운다

규칙이 바뀔 때마다 다시 돌려야 한다

필드 갱신 허용

엔티티를 가변으로 바꾼다

불변 설계를 판정 하나 때문에 포기한다

집계 시 파생

저장하지 않고 매번 판정한다

집계할 때마다 파싱 비용이 든다

세 번째를 골랐습니다. 판정 규칙은 앞으로도 계속 바뀔 텐데 규칙이 바뀔 때마다 과거를 마이그레이션하는 구조는 오래 버티지 못합니다. 파생으로 두면 재집계 한 번으로 과거까지 새 규칙이 적용됩니다. 걱정했던 파싱 비용은 재 보니 집계 시간에 유의미한 차이를 만들지 않았고, 저장해서 아끼려던 것이 애초에 아낄 가치가 없는 비용이었습니다.

4. 결과물

4.1 새 저장소가 들어와도 그대로 돈다

앞의 선택들은 따로 정한 것처럼 보이지만 하나로 모입니다. 저장소를 분석 대상으로 새로 등록했을 때 무엇을 더 해야 하는가입니다.

선택

그래서 새 저장소에서

기록을 커밋 메시지에 남긴다

저장소만 붙이면 기록이 함께 들어옵니다

판정을 저장하지 않고 파생시킨다

등록 즉시 과거 커밋까지 소급해 집계됩니다

벤더·타입 목록을 정책 데이터로 둔다

기본값으로 바로 돌고 필요하면 덮어씁니다

결과적으로 새 저장소를 등록할 때 따로 준비할 것이 없습니다. 그 팀이 커밋 규약만 지키고 있었다면 등록하는 순간부터 지표가 나오고 과거 커밋도 함께 계산되므로 첫 화면에서 바로 추이를 볼 수 있습니다. 반대로 기록을 별도 저장소에 쌓았다면 저장소마다 연동 작업이 필요했을 것이고, 판정을 저장했다면 새로 붙인 저장소의 과거는 영원히 비어 있었을 것입니다.

4.2 0이 만들어진 이유를 화면이 구분하게

신고된 AI가 0인 멤버를 목록에서 숨길지 그대로 보여줄지도 정해야 했습니다. 숨기는 쪽이 화면은 깔끔합니다. 그런데 숨기면 “안 쓴다”와 “쓰는데 안 적는다”가 화면에서 똑같아집니다. 앞에서 신고율 자체를 지표로 삼기로 했는데 화면이 그 둘을 뭉개면 그 결정이 무의미해집니다. 그래서 0을 숨기지 않고 왜 0인지를 화면이 직접 말하게 했습니다.

No AI use was reported in this period.
Commits alone cannot tell not-used from not-declared.

같은 원칙을 지표를 못 불러왔을 때에도 적용했습니다. 조회에 실패하면 0으로 그리지 않고 불러오지 못했다고 적습니다. 체온계가 36.5를 가리키는 것과 고장 나서 눈금이 멈춰 있는 것은 완전히 다른 상태인데 숫자만 보면 구분되지 않습니다. 조회 실패와 값이 0인 것도 마찬가지입니다.

5. 결론

에이전트를 팀에 들이는 일은 도구를 하나 더 쓰는 것과 다릅니다. 개발 과정에 새 참여자가 들어오는 것이고, 그러면 그 참여자가 무엇을 했는지 남기는 자리를 새로 만들어야 합니다. 이번 작업의 대부분은 파싱이나 집계가 아니라 무엇을 남길지와 무엇을 남기지 않을지를 정하는 데 들어갔습니다.

가장 실용적인 발견은 규약을 사람이 아니라 도구에 심을 수 있다는 것이었습니다. 규약이 중요하다는 말은 예전부터 있었지만 늘 사람의 성실성에 기대는 이야기였고 그래서 대개 지켜지지 않았습니다. 중요한 것이 규약 그 자체가 아니라 규약이 놓이는 위치로 옮겨간 셈입니다. 다만 사람 작업과 에이전트 작업을 섞지 않겠다는 규칙만큼은 사람이 지켜야 합니다.

결국 하고 싶었던 것은 단순합니다. 에이전트가 만든 코드라도 나중에 온 사람이 저장소만 받고 바로 이어받을 수 있고, 그 저장소를 분석에 붙이는 순간 아무 준비 없이 결과와 추이가 보이는 상태입니다. 그러려면 설명은 코드와 같은 자리에 있어야 하고, 규약은 저장소 안에 있어야 하며, 해석은 저장소 밖에서 언제든 다시 할 수 있어야 합니다.

참고 자료

Git – git-interpret-trailers — https://git-scm.com/docs/git-interpret-trailers

GitHub Docs – Creating a commit with multiple authors — https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/creating-a-commit-with-multiple-authors

Conventional Commits 1.0.0 — https://www.conventionalcommits.org/ko/v1.0.0/

toffeeman

Site footer