"Ruby가 가장 AI 친화적인 스택이다"라는 주장은 절반만 사실이다
요약
Ruby/Rails 스택이 AI 코드 생성에 유리한 이유는 '설정보다 관습'이라는 구조적 특성 덕분입니다. 하지만 대규모 코드베이스에서 의존성을 파악하는 '탐색' 능력은 단순 텍text 읽기와 다르며, 구조적 지도를 제공할 때 AI 에이전트의 성능이 비약적으로 향상됨을 실험으로 증명했습니다.
핵심 포인트
- Rails의 관습(Convention)은 AI 모델의 환각을 줄이고 코드 생성을 돕는 핵심 요소임
- 단순 코드 작성(Writing)과 복잡한 코드 탐색(Navigating)은 AI에게 서로 다른 차원의 문제임
- 에이전트에게 파일 간 관계를 담은 구조적 지도(Structural Map)를 제공할 때 탐색 정확도가 급증함
- 대규모 프로젝트에서 AI 에이전트의 영향 범위(Blast radius) 파악을 위한 구조적 데이터의 중요성
지난 1년 동안 모든 Ruby 스레드에서 이 주장을 보셨을 것입니다. Ruby와 Rails가 가장 AI 친화적인 스택이라는 것입니다. 더 적은 토큰 (tokens), 더 적은 환각 (hallucination), 모델이 코드를 깔끔하게 작성한다는 내용입니다.
그 주장의 절반은 다투지 않고 인정하겠습니다. 나머지 절반에 대해서는 13개의 실제 Ruby 코드베이스를 통해 측정해 보았으며, 그 결과 모든 저장소(repo)를 한쪽 또는 다른 쪽으로 나눌 만큼 명확한 경계선이 나타났습니다. 당신의 저장소도 포함해서 말이죠.
사실인 절반: Ruby를 작성하는 것은 해결되었다
정말로 타당한 부분부터 시작하겠습니다.
만 개의 Rails 앱을 본 모델은 당신의 코드를 한 줄 읽기도 전에 모델(model)이 어디에 있는지, 잡(job)이 어디로 가는지, 컨션(concern)이 무엇을 하는지, has_many가 무엇을 의미하는지를 알고 있습니다. 설정보다 관습 (Convention over configuration)은 항상 코드를 읽는 다음 사람을 위해 작성되었습니다. 알고 보니 모델 또한 다음 독자였으며, 관습은 모델이 질문을 던지기도 전에 질문의 절반에 답을 해줍니다.
따라서 "서비스 오브젝트 (service object)를 작성해줘", "스코프 (scope)를 추가해줘", "이 컨트롤러 (controller)를 리팩터링해줘"라고 요청하면 어떻게 될까요? 스택이 모델을 이끌어갑니다. 형태가 이미 알려져 있기 때문에 잘못된 추측이 적고, 루프 (loops)가 더 타이트하며, 환각 (hallucinate)할 요소가 적습니다. Rails 기반으로 구축하는 사람이라면 누구나 이를 경험해 보았을 것이며, AI 친화적이라는 명성은 그렇게 얻어진 것입니다. 저는 이 점을 부정하려는 것이 아닙니다.
저는 아무도 위험하게 묻지 않는 질문에 답하기 위해 이 글을 씁니다.
사실이 아닌 절반: 대규모 환경에서 Ruby 탐색하기
"AI가 Ruby를 작성할 수 있는가"는 결론이 났습니다. 망가진 배포 (deploys)를 일으키는 질문은 다릅니다: AI가 Ruby를 탐색 (navigate)할 수 있는가? 이 모델이 변경되면 무엇이 깨지는지, 누가 이에 의존하는지, 영향 범위 (blast radius)가 어디서 끝나는지에 대한 질문입니다.
유창하게 다룰 때는 읽기 (reading)와 탐색하기 (navigating)가 같은 기술처럼 느껴집니다. 하지만 에이전트 (agent)에게 이 둘은 같은 기술이 아닙니다. 파일을 읽는 것은 로컬 (local) 작업이며, 답은 바로 그 텍스트 안에 있습니다. 탐색하기는 구조적 (structural)인 작업입니다. 답은 파일 사이의 경계, 무엇이 무엇을 호출하는지, 무엇이 무엇을 깨뜨리는지 사이에 존재하며, 단일 파일에는 그 정보가 담겨 있지 않습니다.
그래서 저는 13개의 모든 저장소(repo)에 대해 구조적 질문을 실행했습니다. 매번 동일한 작업이 주어졌습니다: 허브 모델(hub model), 즉 Inbox, MergeRequest, Spree::Order를 가져와서, 삭제(teardown) 변경이 일어나기 전의 모든 의존성(dependent)을 찾는 것입니다. 흩어져 있고 명확하지 않은 의존성들은 수동으로 고정되어 숨겨져 있었습니다. 두 가지 방식(two arms)은 동일한 모델과 동일한 커밋을 사용했습니다: 하나는 grep을 수행하고 추론하는 일반적인 에이전트(agent)였고, 다른 하나는 쿼리할 수 있는 구조적 지도(structural map)를 전달받은 동일한 에이전트였습니다. 숨겨진 정답(key)을 기준으로 평가했을 때, 모든 인용은 file:line 단위로 확인되었습니다.
이제 결과의 형태를 살펴보십시오. 기준 재현율(Baseline recall), 그리고 지도를 사용했을 때의 결과입니다:
→ Chatwoot, Inbox → 0.29 → 0.97
→ Mastodon, Status → 0.28 → 0.83
→ GitLab, MergeRequest → 0.26 → 0.67
→ Discourse, Upload → 0.35 → 0.75
→ Solidus, Spree::Order → 0.40 → 0.68
→ Forem, Article → 0.35 → 0.63
→ Rails, ActiveRecord::Relation → 0.67 → 0.92
그다음 격차(gap)가 줄어듭니다:
→ Lobsters, Story → 0.68 → 0.79
→ Redmine, Issue → 0.67 → 0.78
→ RubyLLM, provider/Chat → 0.76 → 0.80
→ raix → 0.60 → 0.73
→ llm.rb, provider → 0.48 → 0.48
저 하위 그룹이 바로 그 '친화적'이라는 명성의 실체이며, 그대로 유지되고 있습니다. 규모가 작고, 읽기 쉬우며, 의존성을 문자 그대로 명명하고 구현부를 가까이 두는(colocated) 저장소들입니다. 강력한 에이전트는 app/models 전체를 읽어 집합을 찾아내며, 지도는 한두 개의 의존성을 추가해 줄 뿐입니다. llm.rb의 경우, 지도가 정답에 아무것도 추가하지 않았지만, 단지 5분의 1 적은 토큰(token)으로 동일한 정답에 도달했습니다. 그 주장의 절반은 아무런 조건 없이 사실입니다.
하나의 저장소는 어느 그룹에도 속하지 않습니다. langchainrb, 0.24 → 0.41: 모놀리스(monolith)처럼 점수를 기록한 작은 젬(gem)입니다. 모델은 langchainrb를 "알고" 있습니다. 하지만 더 이상 배포되지 않는 버전을 알고 있는데, 이는 해당 젬이 학습 데이터보다 더 빠르게 변화했기 때문입니다. 이 생각을 잠시 간직해 두십시오, 다시 돌아올 테니까요.
최상위 그룹은 가독성(legibility)이 더 이상 확장되지 않는 규모의 동일한 스택입니다. 허브 모델(hub model)의 종속성(dependents)들은 연관 관계(associations), 관심사(concerns), 다형성(polymorphism), 그리고 설정 문자열 레지스트리(config-string registries)를 통해 도달하며, grep으로 검색할 수 있는 공통 토큰이 없는 디렉토리들에 흩어져 있습니다. 단순한 에이전트(agent)는 아주 일부만을 찾아내고는 확신에 차서 멈춰버립니다. 이것은 Chatwoot이 지저분해서 발생하는 문제가 아닙니다. 오히려 그 반대입니다. 책에서 가르치는 방식대로 분해(decompose)되었기 때문에, 그 분해된 방식이 종속성들을 grep의 탐색 범위 밖으로 흩어놓는 것입니다.
이는 두 가지 규모에서 발생하는 하나의 문제입니다. 기준선 아래에서는 모델이 전체 리포지토리(repo)를 읽기 때문에 지도가 중복됩니다. 기준선 위에서는 모델이 코드베이스의 샘플을 바탕으로 작업하지만, 어떤 샘플을 사용하는지는 알려주지 않습니다.
그 결과표에서 예상치 못했던 두 가지
가장 작은 모델이 두 번째로 큰 성능 향상을 보였습니다. 저는 다섯 가지 모델(프런티어 모델 2개, 오픈 모델 3개)을 대상으로 벤치마크를 실행했습니다. 성능 향상 폭은 다음과 같습니다: 최상위의 Opus가 +0.26을 기록했고, 바로 그 뒤를 이어 이 세트에서 가장 작은 오픈 모델인 Devstral이 +0.24를 기록하며 제가 실행한 다른 모든 모델(다른 프런티어 모델 포함)보다 앞섰습니다. 이유를 알기 전까지는 이것이 거꾸로 된 것처럼 보일 것입니다. 지도는 모델이 자신의 머릿속에 담을 수 없는 것을 대신해 줍니다. 모델이 담을 수 있는 것이 적을수록, 지도의 가치는 더 높아집니다. 모델 크기와 리포지토리 크기는 양 끝에서 당기는 동일한 레버(lever)와 같습니다.
가장 유명한 리포지토리가 가장 어려운 것 중 하나였습니다. Forem은 인터넷에서 가장 많이 읽히는 Ruby 코드베이스 중 하나인 dev.to 자체를 구동합니다. 모델이 이를 가장 잘 처리할 것이라고 추측하겠지만, 이는 경계해야 할 사례입니다. 프런티어 모델은 이를 부분적으로 암기하고 있어서, 도구를 전혀 사용하지 않고도 학습 데이터로부터 허브 모델의 종속성을 암송할 수 있습니다. 승리는 유지되었지만, 변동성(variance)이 주는 교훈은 분명합니다. 모델이 리포지토리를 기억에 의존해 암송하더라도 _오늘날_의 연결 방식은 놓칠 수 있으며, 당신은 지금 어떤 결과(기억인지 실제 코드인지)를 얻고 있는지 알 수 없다는 점입니다.
Rails는 동일한 효과의 깔끔한 버전입니다. 모든 모델은 Rails 소스 코드를 암기하고 있으며, 지도의 전체 여백은 쿼리 컴파일러 (query-compiler) 내부, 즉 가중치 (weights)에 포함되지 않은 부분에 자리 잡고 있습니다. 그리고 앞서 언급한 모놀리스 (monolith)처럼 점수를 받았던 gem인 langchainrb는 구식 버전입니다. 모델은 더 이상 제공되지 않는 langchainrb를 암송하고 있습니다. 명성은 저장소 (repo)를 더 많이 암송하게 만들 뿐, 더 잘 이해하게 만들지는 않습니다.
당신이 도달하려는 탈출구, 그리고 그것이 닫혀 있는 이유
이런 글을 읽는 모든 사람은 조용히 자신을 예외로 둡니다. 내 코드는 깔끔하다. 내 앱은 잘 구조화되어 있다. 무언가 숨겨져 있다면 나는 알 수 있을 것이다. 나는 하위 그룹에 속한다.
어쩌면 당신이 그럴지도 모릅니다. 오늘날에는 말이죠. 하지만 그 경계선은 고정되어 있지 않습니다.
당신의 저장소가 모델이 한 번에 담을 수 있는 수준을 넘어서는 날, 당신은 grep만으로 충분한 절반의 영역에서 그렇지 않은 절반의 영역으로 이동하게 됩니다. 에이전트 (agent)의 규모를 초월했을 때 알려주는 지원 중단 (deprecation) 경고 같은 건 없습니다. 당신은 항상 그렇듯, 리뷰를 무사히 통과한 diff가 3주 후 운영 환경 (production)에서 문제를 일으키는 것을 보고서야 알게 됩니다.
그리고 그 경계선은 위안을 주는 이야기와는 반대 방향으로 움직이고 있습니다. 모델은 계속해서 읽기 능력이 좋아지고 있으며, 이것이 소규모 저장소 영역이 비기고 있는 이유이자 그 비기는 상황이 점점 더 안전해지는 이유입니다. 하지만 당신이 실제로 배포하는 앱은 컨텍스트 윈도우 (context windows)가 확장되는 속도보다 더 빠르게 성장하며, 앱이 성장함에 따라 의존성 (dependents)들은 더 멀리 퍼져 나갑니다. 지도가 승리하는 보드(board)의 절반은 실제 운영 업무를 설명하는 영역이며, 그 영역은 점점 더 커지고 있습니다.
이것이 바로 "더 나은 모델을 기다리자"는 말이 해결책이 될 수 없는 이유이기도 합니다. 더 나은 모델은 더 많이 읽을 수 있으므로 경계선을 뒤로 밀어냅니다. 하지만 당신의 코드베이스 (codebase)가 성장하므로, 경계선은 다시 더 멀리 밀려납니다. 지도는 약한 모델이 영원히 약할 것이라는 데 거는 도박이 아닙니다. 그것은 저장소 특화적이며 (이 커밋 시점의 당신의 코드로 구축되어 어떤 모델도 암기하지 못한 것), 당신의 코드가 변경됨에 따라 최신 상태를 유지하며, MCP를 통해 모든 에이전트에 서비스를 제공하므로 모델을 교체하더라도 당신을 고립시키지 않습니다.
당신이 어느 영역에 있는지 추론만으로는 알 수 없습니다. 테스트해 보세요.
당신의 코드에 직접 적용해 보는 데 몇 분이면 충분합니다.
당신의 앱에서 신중한 오후 시간을 할애할 만큼 중요한 모델을 하나 고르세요. 그리고 당신의 에이전트에게 차갑게 물어보세요. "이 모델이 해체(torn down)되는 방식을 변경하기 전에, 이 모델에 의존하는 모든 곳을 찾아내." 에이전트가 grep(텍스트 검색)하고 추측하는 과정을 지켜보세요. 그리고 그것이 찾아낸 개수를 세어보세요.
그다음, 지도를 건네주세요.
→ curl -fsSL https://luuuc.github.io/sense/install.sh | sh
→ 당신이 가장 잘 아는 앱의 루트(root) 디렉토리에서 sense scan 실행
→ 에이전트를 연결하기 위해 sense setup 실행
다시 한번 물어보고, 차이(diff)를 확인하세요. 결과가 작고 깔끔한가요? 그렇다면 당신은 무승부(tie)를 목격하게 될 것이며, 그것은 실제적이고 신뢰할 수 있는 답변입니다. 즉, 당신의 리포지토리(repo)는 친화적인 영역에 있으며 이제 걱정을 멈춰도 된다는 뜻입니다. 규모가 큰 앱인가요? 격차가 벌어지는 것을 지켜보세요. 어떤 결과든, 당신은 평판이 줄 수 없는 수치, 즉 당신이 실제로 어느 쪽 영역에 속해 있는지를 얻게 될 것입니다.
전체 보드, 방법론, 그리고 13개 항목 전체에 대한 가공되지 않은 데이터.
저는 이 실험에서 지도를 직접 만들었으므로, 여기서 중립적인 서술자는 아닙니다. 코드, 테스트 하네스(harness), 고정된 커밋(pinned commits), 그리고 트랜스크립트(transcripts)는 모두 공개되어 있으므로, 직접 다시 실행해 보고 저의 수치보다 당신 자신의 수치를 믿으세요.
추신: 자신의 리포지토리가 깔끔한 영역에 있다고 가장 확신하는 엔지니어들도 절반 정도는 맞습니다. 나머지 절반은 일주일 뒤에 저에게 스크린샷을 보냅니다. 당신이 어느 쪽인지 확신하기 전에 직접 실행해 보세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기