본문으로 건너뛰기

© 2026 Molayo

HN분석2026. 06. 10. 13:45

Grit: 에이전트(Agents)를 사용하여 Rust로 Git을 다시 작성하기

요약

에이전트 군단(swarm of agents)을 활용하여 기존 Git을 Rust 기반의 라이브러리 형태로 재구현한 'Grit' 프로젝트를 소개합니다. 방대한 테스트 스위트를 통과하며 메모리 안전성과 모듈성을 확보하는 과정을 다룹니다.

핵심 포인트

  • 에이전트 군단을 활용한 소프트웨어 재구현 실험
  • Git의 방대한 테스트 스위트(42,000개 이상)를 통과하는 것이 핵심
  • C 기반 Git을 Rust 기반의 재진입 가능한 라이브러리로 전환
  • 메모리 안전성과 모듈성을 갖춘 순수 Rust 코어 라이브러리 지향

몇 달 전, 저는 Anthropic이 작동 가능한 C 컴파일러를 작성하기 위해 에이전트 군단(swarm of agents)을 출시하는 실험에 대해 읽었습니다. 그리고 우리가 GitHub를 시작한 지 얼마 되지 않았을 때부터 제가 15년 동안 꿈꿔왔던 일, 즉 Git을 라이브러리 기반으로 처음부터 다시 작성하는 일을 동일한 방식으로 달성하는 것이 가능한지에 대해 궁금해졌습니다.

Git은 물론 매우 복잡한 소프트웨어입니다. 수많은 "플러밍 (plumbing)" 명령어와 수많은 상위 수준 명령어들이 존재하며, 지난 20년 동안 크고 작은 프로젝트를 위해 수천 명의 사람에 의해 점진적으로 구축되었습니다. Git은 링크 가능하고 재진입(reentrant) 가능한 라이브러리를 기반으로 만들어진 것이 아니라, 더 단순한 명령어들을 체이닝(chaining)하는 "Unix" 철학을 기반으로 만들어졌습니다. 이는 모든 작업에 대해 fork/exec 오버헤드 없이 장기 실행 프로세스에서 Git을 사용하기 어렵다는 것을 의미합니다.

하지만 흥미롭게도, Git 프로젝트에는 모든 것이 어떻게 작동해야 하고 작동하지 않아야 하는지를 매우 견고하게 정의하는 1,400개 이상의 스크립트와 42,000개 이상의 테스트로 구성된 매우 포괄적인 테스트 스위트(test suite)가 존재합니다.

만약 Anthropic이 처음부터 만드는 C 컴파일러에 사용했던 것과 동일한 기본 아이디어를 사용한다면 어떨까요? 완전히 새로운 구현을 시작하고, 이를 Rust 라이브러리로 설계한 다음, 에이전트 군단을 이 문제에 투입하여 모든 테스트를 통과할 때까지 계속해서 몰아붙이는 것입니다.

저는 지난 몇 달 동안 간헐적으로 이 작업을 수행했고, 그 결과물은 Grit입니다. Grit는 Git 전체 테스트 스위트의 99% 이상을 통과하는, 처음부터 다시 만든 라이브러리 기반의 메모리 안전(memory-safe)하고 관용적인(idiomatic) Rust 기반 Git 재구현체입니다.

The new Grit project website

Achtung! Grit는 테스트를 통과하지만, *검증(tested)*된 것은 아닙니다. 아직 아무도 이것을 실제 용도로 사용하지 않았습니다. 만약 이것을 사용해 보신다면, 현재로서는 잘못된 동작을 하거나 심지어 데이터를 손상시킬 가능성이 높다는 점을 경고합니다. 사용 시 발생하는 위험은 본인 책임입니다. 하지만 무언가를 발견하신다면, 저희가 수정할 수 있도록 알려주세요.

왜 누군가가 이런 일을 할까요?

Anthropic의 실험과는 달리, 이것은 단순히 그것이 가능한지를 확인하기 위한 것이 아니었습니다. 시작할 때, 저는 만약 이것이 제대로 작동한다면 C 기반의 Git이 가진 문제점들을 해결하는 실제로 꽤 유용한 무언가를 얻을 수 있을지도 모른다고 생각했습니다. 하지만 그 점에 대해서는 잠시 후에 다루도록 하겠습니다.

제가 이것을 통해 얻으려고 했던 것은 무엇이었을까요?

제가 원하지 않았던 것은 C Git을 Rust로 단순히 포팅(port)하는 것이었습니다. 사실, 이 작업을 깊게 파고들수록 Git에서 내려졌던 모든 결정을 그대로 재현해야 했는지조차 확신이 서지 않지만, 원래의 목표가 달성되었으니 이제 그 부분은 고민해 볼 수 있는 문제입니다.

제가 원했던 것은 Git 저장소와 정전적으로(canonically), 충실하게 상호작용할 수 있는 순수 Rust 코어 라이브러리였습니다. 재진입 가능(Reentrant)하고, 링크 가능(linkable)하며, 모듈식(modular)이고 포괄적(comprehensive)인 라이브러리 말입니다. 그리고 그 포괄성을 보장하기 위한 방법으로, 해당 라이브러리를 사용하여 가능한 한 많은 Git 테스트 스위트(test suite)를 통과할 수 있도록 CLI 인터페이스를 구현하는 독립적인 크레이트(crate)를 만드는 것이었습니다.

이것이 저희가 해낸 일입니다.

완벽한가요?

글쎄요, 아니요. 하지만 흥미롭고 논쟁의 여지 없이 이미 유용합니다.

먼저, 몇 가지 주의 사항이 있습니다.

의도적으로 설정한 것이긴 하지만, 실제로 모든 단 하나의 테스트를 통과하고 있지는 않습니다. 이와 같은 라이브러리에서 재현할 가치가 없다고 생각되는 부분들—이메일 관련 작업, i18n(국제화), Perforce/SVN 임포터, 일부 midx/bitmap 관련 기능 등—은 테스트 스위트의 일부를 "건너뛰기(skipped)"로 표시했습니다. 하지만 이 글을 읽는 거의 모든 사람에게 관련이 있다고 확신하는 모든 부분에 대해서, Grit 라이브러리/CLI는 이제 Git 테스트 스위트를 완전히 통과할 수 있습니다.

이것이 완벽하다는 뜻일까요? 아뇨. 여전히 꽤 느리고(어떤 경우에는 기하급수적으로), 테스트되지 않은 기능들이 있으며, API가 매우 깔끔하지도 않고, Windows 빌드도 지원하지 않는 등 부족한 점이 많습니다. 이것은 첫 번째 시도의 첫 번째 마일스톤(milestone)일 뿐입니다.

하지만 몇 달간의 작업과 수십억 토큰(tokens) 분량의 작업치고는 꽤 흥미로운 시작점입니다.

결과물을 보여주세요!!

이것으로 무엇을 할 수 있을까요? 흥미로운 프로젝트였지만, 단순히 가능한지 확인하기 위해 진행한 것은 아닙니다. 저는 Grit이 꽤 유용한 무언가로 쉽게 발전할 수 있다고 생각합니다.

제가 이것을 사용하고 싶은 주요 용도 중 하나는 복잡한 push/fetch 기능을 GitButler나 네트워크 기능이 필요한 다른 독립형 Git 도구들(Jujutsu와 같은)에 번들링(bundle)할 수 있게 하는 것입니다.

현재 Gitoxide와 libgit2의 네트워킹 기능은 부분적이거나, 느리거나, 혹은 존재하지 않습니다. GitButler와 Jujutsu 모두 데이터를 push하거나 pull하기 위해 Git을 포크(forking)하여 실행하는 방식에 의존하고 있습니다. 이에 대한 큰 이유 중 하나는 관련된 자격 증명(credential) 로직이 믿기지 않을 정도로 복잡하기 때문이지만, 이 모든 것은 (이론적으로) 현재 Grit에서 다뤄지고 있습니다.

또 다른 가능한 사용 사례는 매우 광범위하고 흥미로운 일들을 수행하는 데 사용할 수 있는 WASM 빌드입니다. 예를 들어, Vercel의 edge function에서 거의 모든 Git 명령을 실행할 수 있습니다. 또는 isomorphic-git과 같은 부분적인 구현에 의존하는 대신, Grit의 완전한 규격 준수(fully compliant) WASM 빌드를 사용하여 Cloudflare Artifacts와 같은 것들을 구축할 수도 있을 것입니다.

Git의 일부를 별도의 임베디드 가능한 라이브러리 슬라이스(slices)로 만드는 것은 Rust에서 커스텀 Git 서버나 클라이언트 기능을 구축하는 것도 가능하게 합니다.

에이전트 데스크톱 빌드나 Zed와 같은 에디터에 특정 버전의 Git 전체(또는 필요한 Git만)를 네이티브로 임베드(embed)할 수 있습니다.

Rust로 구현된 모든 Git 기능의 전체 빌드 크기는 현재 약 27M이지만, 그 중 상당 부분이 라이브러리이기 때문에 기능 도메인별로, 즉 특정 작업을 수행하는 서브크레이트(subcrates)로 쉽게 분리될 수 있습니다. 아마도 필요한 서브셋(subset)만 간단히 사용할 수도 있을 것입니다.

이 모든 것이 현재의 Grit로 가능한 것은 아니지만, 저는 이 이정표(milestone)가 조금 더 노력한다면 분명히 도달 가능한 범위 내에 있음을 증명한다고 믿습니다.

WASM이라는 말에 혹했네요...

안전성 (Safety)

세부 사항으로 들어가기 전에, 거의 모든 코드가 메모리 안전(memory safe)하다는 점은 주목할 만한 흥미로운 사실입니다.

본질적으로 FFI glue를 통해 C와 통신해야 하는 모듈(날짜/시간)이 하나 있고, TTY 체크가 하나 더 있을 뿐입니다. (듣기로는 TZ 환경 변수를 준수하는 localtime_r / strftime / mktime에 상응하는 순수 Rust 구현체가 없어서, 해당 부분에 대한 FFI는 피할 수 없는 것으로 보입니다.)

Grit의 나머지 모든 부분은 안전한 Rust (safe Rust)입니다.

어떻게 만들었나요?

사실 이것은 약간의 에이전트적(agentic)인 여정이었습니다. 처음에는 일종의 에이전트 파일을 정의하고 여러 에이전트를 루프(loop)로 실행하면 며칠 안에 모든 일을 끝낼 수 있을 것이라고 생각했습니다.

하지만 적어도 이 정도 복잡성을 가진 프로젝트에서는 그렇게 작동하지 않는다는 것이 밝혀졌습니다. 혹은 제가 그것을 잘 못하는 것일 수도 있지만, 분명히 배우기에는 꽤 비용이 많이 들고 매우 좌절감을 주는 비결정론적(non-deterministic)인 교훈이었습니다.

이 과정의 모든 단계를 나열하는 대신, 이 작업을 하면서 배운 몇 가지 사항을 TLDR(요약)로 전달하는 것이 더 흥미로울 것 같습니다.

에이전트는 속이기를 좋아합니다

만약 에이전트에게 "이 Git 테스트들을 통과시켜라"라고 말한다면, 에이전트는 단순히 Git으로 전달하여 작업을 수행하는 간단한 함수를 작성하고 싶은 강한 유혹을 느낍니다. 너무 많은 테스트가 너무 빨리 통과되는 것을 몇 번 목격하고 나서야, 그것이 속임수라는 것을 깨닫고 에이전트가 그런 행동을 정말로 멈추도록 AGENTS 파일을 개선했습니다.

이것은 마치 지니에게 소원을 비는 것과 같습니다. 기본 규칙을 매우 명시적으로 정해야 합니다. 제기랄, 소원을 더 빌어달라는 식의 소원은 안 됩니다.

좋은 예시가 하나 있습니다. sha256 지원을 위해 이를 테스트하는 몇 군데가 있는데, 만약 오직 테스트가 실행되도록만 구현한다면, LLM들은 실제로 sha256 지원을 구현하지 않고도 통과할 수 있다는 사실을 깨달았습니다.

Grit이 실제로 sha256으로 초기화된 저장소에서는 전혀 작동하지 않는다는 것을 깨닫고 Claude에게 물었을 때, 다음과 같은 보고를 받았습니다:

몇 가지 테스트는 실제로 실행됩니다.

git init --object-format=sha256

(t0001-init, t1900-repo-info, t0610-reftable-basics). 이 테스트들은 실제로 실행되고 통과하지만, 그것들이 무엇을 단언(assert)하는지 보십시오 (t0001:519-587): 모든 테스트가 단지 rev-parse --show-object-formatsha256을 보고하는지만 확인하고 있습니다.

, 즉 init이 설정(config)에 extensions.objectformat=sha256을 작성했는지를 확인하고 있다는 것입니다. grit의 init.rs는 해당 메타데이터를 올바르게 처리합니다 (is_known_object_format은 869-870행에서 "sha256"을 허용함). 하지만 그들 중 누구도 해당 저장소(repo)에서 add, commit, 또는 log를 수행하지 않았습니다.

따라서 테스트는 무엇이 테스트되는지를 살펴보고 그것이 작동하도록 만들지만, 실제로는 단지 일반적인 sha1 작업을 수행할 뿐입니다. 시스템이 실제로 "실제 sha256 지원을 구현해야겠다"라고 생각하는 것은 아닙니다.

에이전트(Agents)는 자신이 무엇을 망가뜨렸는지 알지 못한다

이 프로젝트가 몇 주 이상의 시간이 걸린 이유는, 병렬로 작동하는 에이전트 그룹 중 하나가 테스트 하네스(testing harness)의 근본적인 부분을 망가뜨려 대규모 회귀(regression)가 발생한 것처럼 보였고, 그 때문에 중간에 거의 포기할 뻔했기 때문입니다.

저는 과도한 병렬 작업이 이득보다 해를 더 많이 끼치고 있으며, 어쩌면 이 프로젝트를 완수하는 것이 불가능할 정도로 비용이 많이 드는 일일지도 모른다고 생각했습니다. 그래서 한동안 프로젝트를 거의 완전히 포기했습니다.

여기에 제가 시작한 4월 1일부터 오늘까지의 대략적인 타임라인과 그 기간 동안의 테스트 스위트(test suite) 통과율을 나타내었습니다.

보라색 막대는 일일 커밋(commit) 수이며, 이를 통해 두 차례의 집중적인 노력 구간—4월 초의 약 일주일과 6월 초의 약 일주일—을 확인할 수 있습니다.

Commits per day and percentage test-suite passing

점선은 보고된 통과율입니다. 4월 중순에 통과율이 급격히 떨어지는 것을 볼 수 있는데, 이로 인해 프로젝트에 대한 저의 흥미도 떨어졌습니다.

6월 초에 저는 더 단순한 기능을 수행하도록 작업을 구조화하여 기존 작업을 살려보고자 다시 시작했습니다. 그 과정에서 에이전트 중 하나가 실수를 발견하여 테스트 하네스(testing harness)를 수정했고, 통과율이 다시 약 80%로 급등했습니다. 이를 계기로 저는 프로젝트를 끝까지 완수해 보기로 했습니다.

장기적인 멀티태스킹(multitask)은 놀라울 정도로 어렵다

저는 이전에 OpenClaw와 Ralph를 사용해 본 적이 있습니다. 또한 수많은 경우에 병렬 에이전트(parallel agents)를 실행해 보았습니다. 제가 예상했던 것보다 더 어려운 점은 장기 실행(long running)과 병렬(parallel)의 결합입니다.

조정 (Coordination)

아마 흔한 문제는 아닐 것입니다. 그렇게 시도하는 것 자체가 꽤 비용이 많이 들기 때문입니다. 하지만 수십 개의 장기 실행(long running) 에이전트(Agents)가 달려들어 작업할 수 있는 공유된 작업 목록(task list)을 갖는 것은 매우 어렵다는 것을 깨달았습니다.

특히 이를 직접 제어(drive)하고 싶다면 더욱 그렇습니다. 이 경우, 작업을 일시 중지하고, 모든 것을 병합(merge)한 뒤, 방향을 바꾸고 다시 팀을 생성(spawn)해야 합니다.

저는 주로 체크박스가 포함된 공유 플랜 파일(plan file)을 사용하는 방식을 시도해 보았지만, 상당히 지저분했습니다. 아마 Linear나 GitHub issues 같은 도구가 조정(coordination)을 위한 더 나은 방법이겠지만, 이는 더 느리고 네트워크 액세스, 인증 및 모든 클라이언트에서의 도구(tooling)를 필요로 합니다.

프로젝트 막바지에는 제가 만든 Ticgit 로컬 티켓팅 시스템(local ticketing system) 프로젝트를 사용하기 시작했습니다. 덕분에 작업 목록을 로컬에서 쉽게 수정하고 Git으로 옮길 수 있었지만, 이는 별도의 블로그 포스트에서 다루겠습니다.

리소스 관리 (Resource management)

어딘가에 있는 성능 좋은(beefy) 서버에서 이 작업을 실행했어야 했지만, 대신 노트북, Mac studio, Hostinger slice, Cursor cloud agents 등 다양한 환경에서 실행했습니다. 처음 세 곳은 각각 다양한 병렬성(parallelism) 부하에서 리소스 문제를 겪었습니다. Rust를 컴파일하는 것이 한 번에 여러 개를 시도할 때 예상했던 것보다 약간 더 많은 자원을 요구할 수 있다는 사실이 밝혀졌습니다.

에이전트 자체는 문제를 디버깅하고 수정하는 데(스왑 스래싱(swap thrashing), CPU 스래싱(cpu thrashing) 등에 직면했을 때) 상당히 능숙한 편이었지만, 상황이 가끔 변하기도 했고 예상보다 관리하기가 더 어려웠습니다. Anthropic은 컨테이너(containers) 환경에서 컴파일러 실험을 진행했으므로, 저의 'yolo' 방식보다는 사전에 시스템 계획을 세우는 것이 더 나은 아이디어였을지도 모릅니다.

핸드오프 (Handoff)

진행 중인 작업(work in progress)을 넘겨주는 것(handing off)은 지속적인 문제였습니다. 저는 여러 시스템에서 작업을 수행했는데, 그중 일부는 실제로 실행하고 테스트할 수 있는 제 노트북에서 이루어졌기 때문에, 제가 작업하던 상태를 묶어서 다른 곳으로 쉽게 가져가서 이어갈 수 있었다면 좋았을 것입니다.

일부 에이전트 하네스 (agent harnesses)들이 다양한 정도로 이와 유사한 기능을 제공하고 있지만, 저는 여러 프로바이더 (providers)를 사용하고 있었기 때문에 (구독 서비스를 이용해야 하니까요), 여전히 많은 마찰 (frictions)이 있었습니다. 이는 저희가 하네스 종속 (harness lock-in) 레이어가 아닌 VCS 레이어에서 GitButler를 통해 제공하기 위해 작업 중인 부분이니, 계속 지켜봐 주시기 바랍니다.

비용과 토큰 사용량은 빠르게 쌓일 수 있습니다.

정말 주의해야 합니다.

제가 Cursor와 Anthropic (제가 사용한 주요 프로바이더들) 사이에서 정확히 얼마를 썼는지는 모르겠지만, 아마 대략 1만 달러에서 1만 5천 달러 사이였을 것입니다.

저의 접근 방식은 여러 번 바뀌었는데, 이는 부분적으로 작업들을 병렬로 처리하거나 오래 실행하는 데 발생하는 어려움을 목격했고, 비용 대비 테스트 통과 비율 (cost:test-passing ratios)이 변하는 것을 인지했기 때문입니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 HN AI Posts의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0