본문으로 건너뛰기
피드

Bun, Zig에서 Rust로 갈아탔다: 11일짜리 초대형 런타임 포팅 실험

backend 약 17분
vote
0
댓글
북마크

Bun 팀이 53만 줄이 넘는 Zig 코드베이스를 Rust로 기계적으로 포팅했고, Bun v1.4.0부터 Rust 기반으로 전환될 예정임. 핵심 이유는 성능보다 안정성이었고, use-after-free, double-free, 메모리 누수 같은 문제를 컴파일러와 타입 시스템으로 더 빨리 잡기 위해서였음. Claude Code 워크플로 64개를 병렬로 돌려 11일 만에 전 플랫폼 테스트 통과까지 밀어붙인 사례라, LLM 기반 대규모 코드 마이그레이션의 꽤 강한 신호탄임.

  • 1

    Bun은 Zig로 시작했지만 GC 기반 JavaScript 런타임과 수동 메모리 관리가 섞이면서 안정성 문제가 누적됐음

  • 2

    Rust 전환은 성능 개선보다 메모리 안전성, 자동 cleanup, 컴파일러 피드백 루프를 얻기 위한 결정이었음

  • 3

    Claude Code 기반 동적 워크플로를 11일간 병렬 실행해 1,448개 Zig 파일을 Rust로 포팅했고, 비용은 API 가격 기준 약 16만5000달러였음

  • 4

    Bun v1.4.0은 v1.3.14에서 재현되던 버그 128개를 고쳤고, Linux와 Windows 기준 바이너리 크기도 약 20% 줄었음

  • Bun이 결국 Zig에서 Rust로 갈아탐. 이유는 멋진 리라이트 욕망이 아니라, 런타임 안정성 문제를 더 이상 땜질로 버티기 어렵다고 봤기 때문임

    • Bun은 JavaScript, TypeScript, CSS transpiler와 bundler, npm 호환 패키지 매니저, Jest 스타일 테스트 러너, Node.js API 구현, HTTP/WebSocket 클라이언트까지 한 몸에 들고 있는 꽤 큰 시스템임
    • CLI 월간 다운로드는 2200만 회 이상이고, Claude Code, OpenCode, Vercel, Railway, DigitalOcean 같은 곳에서도 실제로 쓰는 런타임이 됐음
  • 문제는 Bun의 범위가 커질수록 메모리 안정성 이슈가 계속 튀어나왔다는 점임

    • Bun v1.3.14에서 고친 버그만 봐도 node:zlib, node:http2, UDP socket, Buffer, crypto.scrypt, fs.watch, CSS parser, BroadcastChannel 주변의 use-after-free, double-free, out-of-bounds, memory leak이 줄줄이 나옴
    • 이미 ASAN, ReleaseSafe, Fuzzilli 기반 24시간 fuzzing, end-to-end 메모리 누수 테스트까지 돌리고 있었는데도 이런 버그가 계속 나왔다는 게 포인트임

중요

> Bun 팀이 Rust로 옮긴 핵심 이유는 'Rust가 더 빠르다'가 아니라 '컴파일러가 메모리 수명 문제를 더 일찍 때려준다'였음. 테스트에서 발견하면 이미 늦고, 리뷰로 막기엔 사람이 너무 쉽게 놓침.

  • Zig가 나빴다는 얘기는 아님. 오히려 작성자는 Bun이 여기까지 온 건 Zig 덕분이라고 꽤 명확히 말함

    • Bun 초기 버전은 작성자가 Oakland의 좁은 아파트에서 1년 만에 Zig로 만들었고, 본인 말로는 Zig가 아니었으면 그 범위를 절대 못 만들었을 거라고 함
    • 다만 JavaScript 런타임은 garbage-collected language 위에서 돌아가고, JavaScriptCore나 V8 같은 엔진은 exception handling과 garbage collector 규칙이 빡셈
    • 여기에 Zig/C 스타일 수동 메모리 관리가 섞이면 “이 포인터는 GC가 보는가?”, “이 버퍼는 누가 free 하는가?”, “예외 경로에서도 cleanup 되는가?” 같은 질문이 모든 allocation마다 따라붙음
  • Rust를 고른 이유는 Bun의 실제 버그 목록이 Rust의 강점과 너무 정면으로 맞닿아 있었기 때문임

    • use-after-free, double-free, error path에서 free 누락 같은 버그는 safe Rust에서는 대체로 컴파일 에러 쪽으로 밀려남
    • Drop 덕분에 값이 scope를 벗어날 때 cleanup이 자동으로 실행되고, Bun 입장에서는 Zig의 defer를 모든 call site에 정확히 심어야 하는 부담이 줄어듦
    • C++도 후보였지만, 여전히 스타일 가이드와 코드 리뷰에 많이 의존해야 해서 “컴파일러가 강제한다”는 피드백 루프가 약하다고 봄
  • 근데 일반적인 리라이트는 보통 망하는 선택지임. Bun도 Zig 코드만 주석 제외 535,496줄이라 사람이 손으로 하면 1년짜리 프로젝트였음

    • 작성자 추정으로는 코드베이스를 완전히 이해하는 엔지니어 3명이 1년은 잡아야 했고, 그동안 버그 수정, 보안 수정, Node.js 호환성 개선, 기능 개발이 사실상 멈춤
    • 그래서 목표는 “Rust답게 예쁘게 다시 짜기”가 아니라 “Zig 코드를 Rust로 transpile한 것처럼 최대한 기계적으로 옮기기”였음
    • Bun v1.4 이후에 unsafe를 줄이고 idiomatic Rust로 다듬는 쪽을 택함
  • 여기서 LLM이 들어옴. 작성자는 Claude Code의 동적 워크플로 약 50개를 11일 동안 계속 돌려서 포팅을 진행함

    • 먼저 Zig 패턴과 타입을 Rust 패턴과 타입으로 매핑하는 PORTING.md를 만들었고, struct field lifetime을 분석해 LIFETIMES.tsv까지 만들었음
    • 곧바로 전체 1,448개 .zig 파일을 옮긴 게 아니라, 처음엔 3개 파일만 trial run으로 포팅하고 reviewer가 Zig와 Rust 동작 차이를 잡게 함
    • 이후 4개 worktree로 쪼개고, 각 workflow마다 16개의 Claude를 붙여 총 64개 Claude가 동시에 움직이는 형태까지 감
sequenceDiagram
    participant 작성자
    participant 구현_클로드
    participant 리뷰_클로드
    participant 테스트_스위트
    participant CI
    작성자->>구현_클로드: Zig 파일과 포팅 규칙 전달
    구현_클로드->>리뷰_클로드: Rust 포팅 결과 리뷰 요청
    리뷰_클로드->>구현_클로드: 동작 차이와 lifetime 문제 지적
    구현_클로드->>테스트_스위트: 수정 후 테스트 실행
    테스트_스위트->>구현_클로드: 실패 stacktrace와 compiler error 반환
    구현_클로드->>CI: 플랫폼별 검증 요청
    CI->>작성자: 전 플랫폼 테스트 통과 신호
  • LLM에게 그냥 “Bun을 Rust로 바꿔줘”라고 던진 게 아님. 핵심은 구현자와 리뷰어를 분리한 적대적 리뷰(adversarial review)였음

    • 구현 Claude 1개에 대해 리뷰 Claude 2개 이상을 붙였고, 리뷰어의 유일한 역할은 “이 코드가 왜 틀렸는지”를 찾는 것이었음
    • 작성자는 사람이 코드 리뷰할 때 작성자와 리뷰어가 다른 것처럼, Claude도 구현 context와 리뷰 context를 분리해야 한다고 봄
    • PR이 +100만 줄짜리라 사람이 전부 읽는 건 말이 안 되고, 대신 “테스트 스위트 + 적대적 리뷰 + 프로세스 자체 수정”으로 신뢰를 쌓음
  • 중간에 삽질도 꽤 현실적임. Claude들이 서로 git stash, git stash pop, git reset HEAD --hard를 치면서 작업을 밟아버린 적도 있음

    • Bun repo가 너무 커서 Claude마다 별도 worktree를 무한정 줄 수도 없었고, 결국 workflow에 “특정 파일 commit 외의 git 명령 금지”, “cargo 같은 느린 명령 금지” 같은 규칙을 박음
    • EC2 인스턴스의 IOPS 기본값을 안 올려서 grep 하나가 디스크 I/O를 몇 분씩 얼리는 문제도 겪음
    • “컴파일 되게 하라”는 지시를 Claude가 “문제 함수 stub 처리”로 해석한 적도 있어서, reviewer에게 “긴 주석으로 workaround를 정당화해야 하면 코드가 틀린 것”이라는 규칙을 추가함
  • 컴파일 에러는 작업 큐로 썼음. 특히 Zig에서는 사실상 하나의 compilation unit이던 코드를 Rust에서 약 100개 crate로 쪼개며 순환 의존성이 터짐

    • 순환 의존성 정리 뒤 compiler error가 약 16,000개 나왔고, 사람 1명에겐 지옥이지만 64개 Claude에게는 병렬 처리 가능한 큐가 됨
    • 각 crate마다 cargo check 결과를 파일별로 묶고, 구현자 1개와 적대적 리뷰어 2개, fixer 1개가 도는 루프를 돌림
    • cargo check는 처음에만 돌리고, 각 Claude가 동시에 느린 명령을 치지 않도록 제한함
  • 테스트 쪽도 그냥 “통과할 때까지 돌림”이 아니었음. Bun 테스트 스위트는 TypeScript로 되어 있어서 런타임 구현 언어와 독립적이라는 게 큰 자산이었음

    • 약 100개 랜덤 테스트 파일을 4개 worktree로 shard하고, 실패 stacktrace를 저장한 뒤 Claude가 고치는 루프를 반복함
    • 테스트 중에는 next dev HMR을 100번 확인하는 통합 테스트, TCP socket 한계를 소진하는 stress test, GB 단위 디스크 I/O, 1만 개 프로세스 spawn 같은 것도 있었음
    • 결국 systemd-run과 cgroups로 메모리, CPU, pid namespace를 제한했지만 그래도 머신이 디스크 부족으로 몇 번 뻗었다고 함
  • 첫 CI 이후 이틀 만에 실패 파일이 972개에서 23개로 줄었고, 하루 반 뒤 Linux가 완전히 green이 됨

    • 이후 Windows 관련 cleanup, unsafe usage 감소, 중복 코드 제거, 플랫폼별 CI 실패 수정 workflow를 계속 돌림
    • 최종적으로 모든 플랫폼에서 Bun 전체 테스트 스위트 100% 통과를 확인하고 main에 merge함
    • 테스트는 0개 skipped, 0개 deleted였다고 명시함. 이건 꽤 중요함

중요

> 이번 포팅은 11일, 6,778 commits, uncached input token 59억, output token 6억9000만, cached input token read 720억이 들어간 작업임. API 가격 기준 비용은 약 165,000달러였음.

  • Rust rewrite가 들어간 Bun v1.4.0은 안정성만 좋아진 게 아니라 성능과 크기도 개선됨

    • Bun v1.4.0은 v1.3.14에서 재현되던 버그 128개를 고침
    • 초기 Rust rewrite만으로 바이너리 크기가 Windows 3.8MB, macOS 5.5MB, Linux 6.8MB 줄었고, 이후 ICU 데이터 최적화와 identical code folding까지 합쳐 Linux와 Windows에서 약 20% 줄었다고 함
    • Linux x64에서 Bun v1.3.14 대비 v1.4.0은 여러 benchmark에서 2%~5% 빨라졌고, Rust와 C/C++ 간 link-time optimization 덕분에 언어 경계를 넘는 inlining도 가능해짐
  • 메모리 사용량 개선은 특히 개발 서버나 build loop에서 체감될 수 있는 부분임

    • 기존 Bun.build()는 같은 60-module 프로젝트를 한 프로세스에서 2,000번 bundle하면 build마다 약 3MB씩 영구 누수됐음
    • dev server처럼 요청마다 bundle하는 도구라면 결국 메모리가 계속 차오르는 패턴임
    • Rust 버전에서는 Drop과 개선된 LeakSanitizer 연동 덕분에 이런 누수를 잡고, 메모리가 일정 수준에서 안정화됨
  • Rust로 옮겼다고 버그가 0개가 된 건 아님. 포팅 과정에서 알려진 regression 19개가 생겼고, 모두 고쳤다고 함

    • Zig의 assert는 함수라 release build에서도 인자가 실행되지만, Rust의 debug_assert!는 release build에서 expression 자체가 사라져 HMR graph 업데이트가 빠진 사례가 있었음
    • Zig helper가 odd byte를 조용히 무시하던 곳을 Rust bytemuck::cast_slice로 옮기면서 panic이 나기도 했음
    • Zig ReleaseFast에서는 bounds check가 빠졌지만 Rust release build는 bounds check를 유지해서, 기존에 숨어 있던 off-by-one 문제가 panic으로 드러난 케이스도 있었음
    • Zig의 comptime format string과 Rust 함수 호출의 차이 때문에 ANSI color marker 처리도 깨졌고, 결국 Rust에서는 macro로 바꿔야 했음
  • 현재 Rust 코드에서도 unsafe는 꽤 남아 있음. 다만 이건 JavaScriptCore 같은 C/C++ 라이브러리를 계속 품고 가는 Bun의 구조상 어느 정도 불가피함

    • 전체 Rust 코드 약 78만 줄 중 unsafe block 내부는 약 27,000줄, unsafe keyword는 약 13,000개 수준임
    • unsafe block의 78%는 C++에서 넘어온 pointer나 C library 호출 같은 한 줄짜리라고 함
    • 팀은 faithful Zig port에서 출발했으니, 앞으로 idiomatic Rust로 refactor하면서 이 비율을 줄일 계획임
  • 포팅 이후에도 자동화는 계속 붙고 있음. Claude Code Security로 11번의 보안 리뷰를 돌렸고, parser fuzzing도 24시간 돌아감

    • JavaScript, TypeScript, JSX, CSS, JSON5, JSONC, TOML, YAML, Markdown, INI, Bun Shell script, semver range, .patch file, CSS color parser까지 fuzzing 대상임
    • fuzzer가 버그를 찾으면 Claude가 재현과 수정 PR을 만들고, 사람은 그 PR을 리뷰함
    • 지금까지 parser 실행 횟수는 1000억 번, 이로 인해 약 15개 PR이 나왔다고 함
  • 결론적으로 이 글은 “LLM이 이제 엔지니어를 대체한다”보다 “좋은 테스트 스위트와 강한 제약을 가진 팀은 LLM을 병렬 작업자로 굴릴 수 있다”에 가까움

    • Bun 팀은 LLM에게 자유도를 무한히 준 게 아니라, PORTING.md, LIFETIMES.tsv, adversarial review, compiler error queue, CI, fuzzing으로 계속 좁은 레일을 깔았음
    • 그래서 이 사례의 핵심은 모델 자체보다도 작업을 쪼개고 검증 가능한 루프로 만드는 엔지니어링 설계임
    • 한 명의 엔지니어가 1년짜리 팀 작업을 11일짜리 고위험 실험으로 압축했다는 점에서, 2026년식 대규모 마이그레이션의 기준선이 확실히 바뀌고 있음

기술 맥락

  • Bun이 Rust를 고른 건 “새 언어로 다시 예쁘게 짜자”가 아니라, GC 런타임과 수동 메모리 관리가 부딪히는 지점을 컴파일러가 더 빨리 잡아주길 원했기 때문이에요. JavaScriptCore 같은 엔진과 붙는 런타임에서는 포인터 수명, 예외 경로, cleanup 누락이 계속 문제로 터지거든요.

  • 포팅 전략도 꽤 현실적이에요. 처음부터 idiomatic Rust로 리팩터링하면 동작 차이가 너무 많이 생기니까, 일단 Zig 코드를 Rust로 기계 번역한 것처럼 최대한 비슷하게 옮겼어요. 그래야 기존 TypeScript 테스트 스위트로 “같은 Bun인가?”를 검증할 수 있었기 때문이에요.

  • LLM 사용 방식의 핵심은 구현보다 검증 루프예요. 구현 Claude, 리뷰 Claude, fixer Claude를 나누고, compiler error와 test failure를 파일 단위 작업 큐로 만들어 병렬 처리했어요. 사람은 모든 코드를 직접 쓰기보다, Claude들이 틀리는 패턴을 보고 workflow 규칙을 계속 고치는 역할을 했고요.

  • 이 사례가 실무적으로 중요한 이유는 테스트 가능한 대형 코드베이스에서 LLM 마이그레이션이 어떤 모양이어야 하는지 보여주기 때문이에요. “모델에게 맡긴다”가 아니라, 테스트 스위트, CI, fuzzing, 적대적 리뷰, 명령 제한까지 묶어서 실패를 좁은 범위에 가두는 구조가 있어야 해요.

이건 단순히 'Rust가 Zig보다 낫다'는 글이 아니라, 대형 시스템에서 언어 선택과 LLM 자동화가 어디까지 현실적인지 보여주는 케이스임. 특히 테스트 스위트, 적대적 리뷰, 컴파일러 에러를 작업 큐로 쓰는 방식은 한국 팀들도 대규모 레거시 전환을 고민할 때 꽤 참고할 만함.

댓글

댓글

댓글을 불러오는 중...

backend

시카고 미술관 API에 숨어 있는 ‘조회수 낮은 작품’ 필드

시카고 미술관 API에는 웹사이트에서 거의 조회되지 않은 작품을 표시하는 불리언 필드가 있다. 기준은 2010년 1월 1일 이후 조회수 200회 미만으로, API 설계 안에 꽤 인간적인 큐레이션 포인트가 숨어 있는 셈이다.

backend

유엔넷 DB 관리 솔루션, 오라클에서 PostgreSQL로 가는 일본 시장 노린다

유엔넷이 일본 PostgreSQL 기술지원 기업 뮤텍과 독점 공급 계약을 맺고 DB 성능 분석 솔루션 타란튜너를 일본에 공급해. 오라클에서 PostgreSQL로 전환하는 기업이 늘면서 SQL 지연, 잠금 경합, 실행 계획 비교 같은 운영 관리 수요를 겨냥한 움직임이야.

backend

유니스와프 ‘듀얼풀 훅’, 비활성 자산에서도 수익을 만들 수 있을까

유니스와프가 듀얼풀 훅 감사를 진행 중이며 완료 후 오픈소스로 공개할 계획이라고 전했다. 이 기능은 활성 자산과 비활성 자산 모두에서 수익 전략을 만들 수 있다는 점에서 DeFi 유동성 구조 변화로 언급되고 있다.

backend

EDB, 포스트그레스 AI에 자율 운영 DB와 실시간 분석 기능 추가

EDB가 포스트그레스 기반 AI 데이터 플랫폼에 에이전틱 데이터베이스와 통합 분석 기능을 추가했다. 데이터 이동 없이 관계형·분석·벡터·에이전트 워크로드를 한 플랫폼에서 처리하고, 운영 지표 200개 이상을 모니터링해 튜닝과 문제 대응을 자동화하는 방향이다.

backend

클로저 1.13 알파, map destructuring에 ‘필수 키 체크’ 추가

Clojure 1.13.0-alpha1이 공개되며 map destructuring에서 필수 키 누락을 바로 잡아낼 수 있는 새 지시자들이 추가됐다. 기존 :keys, :syms, :strs의 체크 버전인 :keys!, :syms!, :strs!를 쓰면 필요한 키가 없을 때 예외를 던질 수 있다.