에이전트가 36,000개의 파일로 구성된 Rails 앱을 감사하는 과정을 기록했습니다: 상세 진행 과정
요약
대규모 Rails 모놀리스 코드베이스에서 코딩 에이전트가 의존성을 감사하는 과정을 실험한 기록입니다. 코드베이스 맵(map) 제공 여부에 따른 에이전트의 성능 차이와 대규모 파일 환경에서의 한계를 상세히 분석합니다.
핵심 포인트
- 36,000개 이상의 파일로 구성된 대규모 프로젝트에서의 에이전트 활용 실험
- 구조 정보(Map)가 없을 때 발생하는 토큰 예산 및 검색 효율성 문제
- Rails의 관례(Convention)가 에이전트의 코드 이해에 미치는 긍정적 영향
- 단순 토큰 검색(grep)만으로는 복잡한 의존성 파악에 한계가 있음
저는 코딩 에이전트에게 현존하는 가장 큰 오픈 소스 Rails 모놀리스 (monolith)에서 실제 유지보수 업무를 맡기고, 전체 세션을 기록했습니다. 그 후 코드베이스의 맵 (map)을 제공하여 동일한 업무를 맡기고 그 과정 또한 기록했습니다.
이어지는 내용은 상세한 진행 과정 (play-by-play)입니다. 에이전트의 움직임은 실제이며, 길이를 위해 약간 다듬어졌습니다. 제 코멘터리는 그 사이의 들여쓰기 된 노트입니다. 첫 번째 실행이 어디서 멈추는지, 그리고 왜 자신이 멈췄다는 것을 인지하지 못하는지 지켜보십시오.
해당 리포지토리 (repo)는 GitLab이며, 고정된 커밋 (pinned commit)에 인덱싱되어 있습니다:
gitlabhq @ 1f9c256f0
tracked files / ruby 68,289 / 29,784
files indexed (coverage) 36,829 (100%)
...
하나의 Rails 앱 안에 수백만 개의 엣지 (edges)가 존재합니다. 그 누구도 이를 머릿속에 다 담아둘 수 없습니다. 질문은 에이전트가 파일로부터 충분한 정보를 재구성하여 업무를 수행할 수 있느냐 하는 것입니다.
두 실행 모두 동일한 작업입니다:
당신은
MergeRequest와 그에 부착된 모든 것이 변경되거나 제거될 때의 동작 방식을 재작업할 예정입니다. 모델 (model)을 건드리기 전에,MergeRequest컨트랙트 (contract)에 의존하는 모든 곳을 감사하십시오.
골드 셋 (gold set)은 먼저 소스 코드를 대상으로 수동으로 구축되었습니다: app/services, app/workers, app/models, app/graphql, app/serializers, 그리고 lib/에 흩어져 있는 16개의 의존성 항목들로, 단 한 번의 grep 명령으로는 모두 찾아낼 수 없습니다. 에이전트는 이 리스트를 절대 보지 못합니다. 대신 이 리스트를 기준으로 평가를 받습니다.
두 실행 모두 동일한 모델 (Claude Code, Opus 4.8)을 사용했습니다. 동일한 프롬프트 (prompt), 동일한 커밋 (commit). 유일한 변수는 맵 (map)의 유무입니다.
실행 1, 콜드 스타트 (cold). grep 노가다.
[00:00] 나 → 재작업 전 MergeRequest의 모든 의존성을 감사하라
[00:06] 에이전트 → grep -rin "merge_request" app/ lib/ ee/ | wc -l
=> 41,800+
구조 정보가 없을 때 취할 수 있는 첫 번째이자 유일한 움직임: 토큰 (token) 검색. 4만 1천 개의 히트 (hits). 이것은 의존성 리스트가 아니라, 또 다른 코드베이스입니다. 에이전트에게는 토큰 예산 (token budget)이 있지, 주말이 있는 게 아닙니다.
[00:31] 에이전트 → grep -rinE "belongs_to :merge_request|has_many :merge_requests"
=> 380개 이상의 이름이 지정된 연관 관계 (associations)
이 부분은 잘 해냅니다. 이름이 지정된 연관 관계 (associations)는 빠르게 반환되며, Rails의 관례 (conventions)가 여기서 그 명성을 입증합니다.
app/services의 서비스,app/workers의 워커처럼 그 역할에 맞게 이름이 지정되어 있습니다. 에이전트는 이를 정확하게 읽어냅니다. 답변의 쉬운 3분의 1 지점입니다.
[01:10] agent → grep -rinE "merge_request_id|\.merge_request\b|MergeRequest\." | wc -l
=> 9,200+
이제 진짜 작업이자 막다른 길입니다. 9,000개의 검색 결과가 나왔고, 에이전트는 이 중 무엇이 의존성 (dependents)이고 무엇이 부수적인 지역 변수 (local variable)나 로그 라인인지 결정해야 합니다. 에이전트는 샘플링을 합니다. 모든 것을 다 읽을 수는 없습니다.
에이전트는 다음과 같은 것들을 그냥 지나쳐 버립니다:
# Issuable은 Issue와 MergeRequest 모두에 섞여 있습니다.
# "the issuable"에 대한 노트를 해결하는 코드는
# grep이 매칭할 수 있는 문자열을 한 번도 입력하지 않고도 MergeRequest에 의존합니다.
...
노트 해결 서비스 (notes-resolution service)는
Issuable컨선 (concern)을 통해MergeRequest에 의존합니다. 클래스 이름을 직접 입력하지 않습니다.MergeRequest에 대한 grep은 여기에 도달하지 못합니다. 중요한 의존성들은 정확히 매칭할 수 있는 공유 토큰이 없는 것들입니다.
[04:50] agent → [감사 내용을 작성함]
"MergeRequest의 의존성을 감사했습니다. 발견됨: 이름이 지정된
연관 관계 (associations), diff 시리얼라이저 (serializer),
파이프라인 (pipeline)
...
5분, 제한 시간(harness ceiling)에 근접했습니다. 에이전트는 내내 정직하게 작업을 수행했고, 차분하고 완성된 듯한 보고서를 생성했습니다. 인용구를 지어내지 않았으며, 언급된 모든 의존성은 실제 존재합니다. 다만 중요한 16개 중 단 2개만 찾아냈을 뿐입니다. 다시 실행했을 때는 1개를 찾았습니다. 에이전트는 자신이 2개에서 멈췄다는 사실을 모릅니다. 출력 결과 중 그 어떤 것도 이를 알려줄 수 없습니다.
냉혹한 결과: 16개 중 2개, 그 다음은 1개. 분산 재현율 (scattered recall) 0.09. 감사는 완료된 것처럼 보였습니다.
마지막 문장이 바로 모든 위험 요소입니다. 작은 리포지토리 (repo)라면 16개 중 2개라는 사실을 알아차릴 것입니다. 하지만 모놀리스 (monolith) 환경에서는 이를 그대로 배포하게 되고, 당신이 놓친 14개는 새벽 3시에 누군가를 호출하게 만드는 바로 그 요소들이 될 것입니다.
2차 실행, 동일한 작업, 지도를 활용함.
[00:00] me → [동일한 프롬프트]
[00:03] agent → sense_blast MergeRequest
...
단 한 번의 호출. 932개의 심볼(symbols), 41,800개의 grep 히트(hits)로부터 재구성하는 대신 단 한 번에 반환된 해결된 집합(resolved set). Issuable 컨선(concern)이 숨겼던 노트 해결 서비스(notes-resolution service)가 바로 저 목록에 있습니다. 맵(map)은 grep이 볼 수 없었던 가장자리(edge)를 따라 걸었습니다.
[00:18] agent → [각 후보를 읽고, 파일:라인에 고정(pins to file:line)]
[03:40] agent → [감사(audit) 작성]
"MergeRequest에 대한 16개 항목 의존성 감사, 각 항목이 고정됨
...
동일한 모델. 동일한 예산(budget)이지만, 탐색(hunting) 대신 읽기와 고정(pinning)에 다르게 사용되었습니다. 가장 성능이 좋았던 실행에서 16개 중 13개를 잡아냈으며, 최저치는 10개였습니다. 발견된 의존성 중 12개는 일반적인 실행(cold runs)에서는 단 한 번도 도달하지 못했던 것들입니다:
auto-merge worker · notes-resolution service · cycle-analytics builder · API discussions · GraphQL issuable · Jira integration · milestone promotion · ghost-user handler · timelog · timeline event · URL builder · enterprise discussion
매핑된 결과: 16개 중 10개, 그 다음 13개. 분산된 재현율(Scattered recall) 0.72, 전체 감사(full audit) 0.26 → 0.67.
점수로는 알 수 없는, 기록을 통해 배운 두 가지
이 과정에서 맵(map)의 비용이 더 많이 들었으며, 저는 이를 숨기지 않겠습니다. 이번 실행에서 맵은 토큰을 더 적게 쓰는 대신 약 9% 더 많이 사용했습니다 (27,604 → 30,128). 다른 리포지토리(repos)에서는 더 저렴하게 수행되었습니다. 토큰 비용은 작업(task)에 따라 달라지며, 저는 에이전트(agents) 간에 이를 결코 비교하지 않을 것입니다. 변하지 않은 것은 도달 범위(reach)입니다: 실제 의존성 집합의 2.6배 더 많은 도달, 그리고 12개의 조용한 결함들(silent breaks). 16개 중 2개에서 13개로 가기 위해 9%의 토큰을 더 사용하는 것은, 발생하지 않았을 사고(incident)와 비교하면 오차 범위 수준에 불과합니다.
이 과정을 처음 실행했을 때 지도가 길을 잃었고, 그것이 바로 제가 지금 이 도구를 신뢰하는 이유입니다. 초기 실행 결과는 12점, 8점, 1점 등 제각각이었습니다. 게으른 읽기(lazy read) 방식으로는 시나리오의 문제라고 치부하기 쉬웠습니다. 하지만 트랜스크립트(transcripts)는 다른 사실을 말해주고 있었습니다: sense_blast가 호출할 때마다 서로 다른 호출자(callers) 집합을 반환하고 있었던 것입니다. 이 정도 규모의 허브에서는 거의 모든 의존성(dependency)이 하나의 신뢰도 점수(confidence score)를 공유하는 일반 메서드 호출(plain method call)이며, 인덱스(index)가 불안정한 정렬(unstable sort)로 해당 결합 목록을 제한하면서, 직접적인(direct) 호출자를 멀리 떨어진 호출자로부터 밀어내고 있었습니다. 대규모 리포지토리(large-repo) 사용자에게 조용히 전달되는, 재현 불가능한 영향 분석(impact analysis)이었던 셈입니다. 수정 작업을 통해 제한(cap)을 결정론적(deterministic)으로 만들었고, 신뢰도 점수를 우선순위로 하되 직접 호출이 간접 호출보다 우선하도록 동점 처리(ties broken) 규칙을 적용하여, 이제 모든 사용자에게 배포되었습니다. 벤치마크는 도구를 평가하기 위한 것이어야 했으나, 도구는 오히려 스스로를 계속 수정하고 있었습니다.
이러한 결정론(determinism)은 이것이 모델의 트릭이 아니라 구조(structure)라는 점을 뒷받침하는 조용한 근거이기도 합니다. 지도는 매번 동일한 932를 계산합니다. 모델은 실행할 때마다 다른 답을 추론하며, 여러분은 그것이 2에서 1로 변하는 것을 목격했습니다. 더 나은 모델은 더 재현 가능하게(reproducibly) 만드는 것이 아니라, 더 확신 있게(confidently) 추론할 뿐입니다. 지도는 훈련 스냅샷(training snapshot)이 아니라 이 커밋(commit) 시점의 리포지토리를 읽으며, 어떤 에이전트(agent)라도 MCP를 통해 이를 호출할 수 있습니다. 이 중 그 어떤 것도 다음 분기에 어떤 모델을 실행하느냐에 의존하지 않습니다. 모델이 바뀌어도 변하지 않는 부분입니다.
직접 기록해 보세요
여러분의 코드에서도 직접 확인해 볼 가치가 있습니다. 여러분의 모놀리스(monolith)에도 MergeRequest가 존재하기 때문입니다.
여러 서비스의 절반이 접근하고 있지만 아무도 완전히 추적하지 않는 대상을 하나 고르세요. 에이전트에게 차갑게 물어보세요. "이 모델이 해체되는 방식을 변경하기 전에, 이 모델에 의존하는 모든 곳을 찾아줘."
grep이 고군분투하는 모습을 지켜보세요. 답변의 개수를 세어보세요.
그다음 지도(map)를 제공하세요.
→ curl -fsSL https://luuuc.github.io/sense/install.sh | sh
→ 밤중에 팀원들을 호출하게 될 앱의 루트(root)에서 sense scan 실행
→ 에이전트를 연결하기 위해 sense setup 실행
다시 질문하고 두 트랜스크립트(transcripts)를 비교(diff)해 보세요. 이 정도 규모의 트리(tree)에서는, 맨 처음 질문했을 때 찾지 못했던 의존성들이 바로 변경 사항으로 인해 장애가 발생했을 지점들입니다.
전체 세션 로그, 정답지, 13개 리포지토리(repos)의 모든 트랜스크립트(transcript).
저는 이 기록물 안에서 지도를 구축합니다. 제가 틀렸다고 판단하는 데 필요한 모든 것—트랜스크립트(transcripts), 하네스(harness), 고정된 커밋(pinned commit), 판정관(judge)—은 모두 공개되어 있으므로, 제 해석을 받아들이기 전에 세션(session)을 직접 확인해 보시기 바랍니다.
추신. 해당 기록 전체에서 가장 무서운 장면은, 16개 중 단 2개의 의존성(dependents)에 대해 구성되고 자신감 넘치는 감사(audit) 보고서를 작성하고는 그대로 종료해 버리는 콜드 런(cold run)입니다. 허둥대는 기색조차 없습니다. 대규모 리포지토리(repo)에서 정말 두려워해야 할 것은 바로 그 침착함입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기