Claude Code로 100만 줄 코드베이스를 옮긴 방법 — Anthropic의 6단계 마이그레이션 프로세스
출처: Anthropic (Claude Blog)
원문: https://claude.com/blog/ai-code-migration
작성자: 팔복소프트-김팔복
한눈에 보기
- Bun의 공동창업자 Jarred Sumner가 Claude Code로 Bun을 Zig에서 Rust로 포팅했다. 2주가 채 안 되는 기간에 약 100만 줄이 생성됐고, 머지 전 CI에서 기존 테스트 스위트를 전부 통과했다.
- Mike Krieger는 주말 동안 Python 코드베이스를 16만 5천 줄의 TypeScript로 옮겼다. 목적은 빌드 시간 단축이었고, 플랫폼당 약 8분 걸리던 컴파일이 약 2초로 줄었다.
- 핵심 원칙은 "코드를 고치지 말고, 그 코드를 만들어낸 루프를 고쳐라"다. 개별 파일을 손으로 패치하는 대신 룰북(rulebook)을 수정하고 해당 배치를 다시 생성한다.
- 비용은 여전히 든다. Bun 마이그레이션은 캐시되지 않은 입력 토큰 59억 개, 출력 토큰 6억 9천만 개를 소비했고 API 가격 기준 약 16만 5천 달러 수준이다.
- Anthropic은 이 과정을 6단계로 정리하고, 일반화된 스타터 킷을 GitHub에 공개했다.
주요 내용
무엇이 달라졌나
언어를 바꾸는 코드 마이그레이션은 얼마 전까지 수년짜리 프로젝트였다. 원문에 따르면 Anthropic 내부에서는 최근 한 달 사이에 개발자 개인이 수만~수십만 줄 규모의 패키지 10개를 옮겼다. 사용한 도구는 Claude Fable 5, Claude Opus 4.8, 그리고 Claude Code의 dynamic workflows다.
대표 사례 두 가지가 소개된다.
Bun: Zig → Rust. 2주 미만에 100만 줄 규모의 코드가 생성됐고, 머지 전 CI에서 기존 테스트가 100% 통과했다. 머지 후 드러난 회귀는 19건이었고 모두 수정됐다. 이 Rust 포팅 버전은 6월에 Claude Code에 탑재돼 출시됐다.
내부 도구: Python → TypeScript. 주말 동안 16만 5천 줄로 옮겼다. 수백 개의 에이전트, 8개의 단계 게이트, 3라운드의 적대적 리뷰, 그리고 모든 명령어 출력을 원본 Python과 diff하는 최종 패리티 검사가 들어갔다.
왜 지금 다시 계산해볼 만한가
마이그레이션을 미뤄온 이유는 대부분 비용과 리스크였다. 병렬 코드베이스를 몇 분기씩 유지해야 하고, 결과가 90% 패리티에 그치면 시작하기 전보다 상황이 나빠진다. 원문은 이제 최악의 경우가 "브랜치를 지우고 다시 시도"로 바뀌었다고 정리한다.
다만 비즈니스 근거는 여전히 필요하다. 과거 4년짜리 100만 줄 프로젝트에 300400만 달러가 들었다면, 지금은 수만수십만 달러 규모다. 대신 그 정당화 기준이 낮아졌다는 점이 포인트다. 체인지로그에 1년치 메모리 버그 패치가 쌓여 있거나 만성적인 병목 하나가 있으면 그것으로 충분하다는 것이다.
실제로 Python → TypeScript 포팅의 동기는 빌드였다. 단일 바이너리로 배포되는 내부 도구였는데 Python 툴체인으로 바이너리를 만드는 데 플랫폼당 8분, 빌드 매트릭스 전체로는 릴리스마다 30분이 걸렸다. 포팅 후에는 컴파일이 약 2초, 바이너리 기동 속도는 6배 빨라졌고 별도 배포 파이프라인 하나를 없앨 수 있었다.
코드 마이그레이션이 AI에 잘 맞는 이유
원문은 네 가지를 든다.
- 병렬성. 파일이나 크레이트 단위로 수천 개의 독립적인 작업으로 쪼갤 수 있다.
- 명세가 이미 존재한다. 기존 코드 자체가 훌륭한 스펙이자 번역 가이드의 레퍼런스가 된다.
- 심판이 내장돼 있다. 테스트 스위트가 있으면 검증이 객관적이다. 사람이 품질을 중재하지 않아도 모델이 며칠씩 정답에 맞춰 갈아넣을 수 있다.
- 작업 큐가 저절로 생긴다. 컴파일러 에러와 테스트 실패가 그대로 다음 작업 항목이 된다.
사전 조건: 심판을 먼저 만든다
시작 전에 필요한 건 강력한 판정 기준(judge)이다. 이게 없으면 종료 조건도, 성공 지표도 없다. 문제는 원본 언어로 작성된 테스트가 포팅 대상에 존재하지 않는 내부 함수에 의존하는 경우가 많다는 점이다.
원문이 제시하는 절차는 이렇다. 먼저 Claude로 기존 테스트를 분류해 외부 호출로 표현 가능한 것과 내부 구현에 묶인 것을 가른다. 다음으로 외부 인터페이스 기반 테스트를 원본과 포팅본 양쪽에서 돌아가는 형태로 다시 쓴다. 이때 적대적 에이전트를 붙여 재작성 과정에서 단정문이 느슨해지지 않았는지 검증한다. 마지막으로 심판 자체를 검증한다. 원본 코드에 돌려서 통과하는지 확인하고, 일부러 망가뜨린 코드에 돌려서 실패하는지도 확인한다. 깨진 코드를 잡아내지 못하면 심판이 아니다.
6단계 프로세스
1단계 — 룰북, 의존성 맵, 갭 인벤토리
순서가 중요하다. 룰북이 갭 인벤토리보다 먼저다. 갭 인벤토리는 "룰북의 기본 규칙으로 커버되지 않는 것"으로 정의되기 때문이다.
룰북의 형태는 초기 아키텍처 결정에 따라 갈린다. 기존 구조를 유지한다면 타입과 관용구를 언어 간 매핑하는 룩업 테이블에 가깝고, 완전히 재설계한다면 설계 문서가 된다. Bun 사례에서는 Claude와 대화하며 모호한 영역마다 정책을 정하는 식으로 룰북을 만들었고, 자주 발생하는 실패 유형 8가지를 각각 검토하는 서브에이전트 8개를 별도로 두었다.
의존성 맵은 병렬 작업 분배를 위해 필요하다. 어떤 파일을 먼저 옮기고 어떤 파일을 같은 배치에 묶을지 결정한다. 매니페스트가 명시적인 언어도 있지만 C/C++나 Python 같은 경우 의존성을 직접 탐색해서 그려야 한다. 이건 에이전트가 결정론적 스크립트를 작성해 실행하게 하는 방식을 쓴다.
갭 인벤토리는 새 언어가 요구하는데 기존 언어에는 없던 것들을 정리한 문서다. Zig → Rust에서는 수동 메모리 관리가, Python → TypeScript에서는 인터페이스와 계약(contract)이 그 지점이었다. Python은 어떤 형태의 객체를 받고 무엇을 반환하는지 선언할 필요가 없지만 TypeScript는 요구한다. 이런 암묵적 지식을 명시적으로 적어두는 작업이다.
2단계 — 규칙을 스트레스 테스트한다
본 마이그레이션에 앞선 미니 마이그레이션, 일종의 시운전이다. Bun 사례에서는 에이전트 하나에 룰북대로 3개 파일을 번역시키고, 다른 에이전트에는 "시니어 Rust 엔지니어처럼" 같은 파일을 번역시킨 뒤, 세 번째 에이전트가 그 diff를 보고 새 규칙을 만들게 했다. 이 단계에서 치명적인 문제 2건을 잡았는데, 그대로 1,448개 파일에 팬아웃됐다면 큰 문제가 됐을 것들이다.
단, 이 방식은 구조 보존형 마이그레이션에서만 통한다. 재설계라면 대신 설계 문서를 적대적 리뷰어로 직접 공격한 뒤 버릴 셈치고 엔드투엔드로 한 번 돌려 검증한다. 어느 쪽이든 이 단계에서 번역된 파일은 전부 버린다. 목표는 규칙을 다듬는 것이지 진도를 빼는 게 아니다.
3단계 — 전체 번역
여기서부터는 구현 → 리뷰 → 수정의 멀티 에이전트 루프가 반복된다. 실무적으로 참고할 만한 포인트가 몇 가지 있다.
- 구현 담당은 작은 모델에 맡기고 리뷰어에 큰 모델을 쓴다. Python → TypeScript 포팅에서는 서브에이전트 12개를 팬아웃하면서 Claude Sonnet을 썼다.
- 작업 큐는 기계적으로 만든다. "번역된 파일이 디스크에 존재하는가"로 완료를 판정하고, 큐를 매번 디스크에서 재구성한다. 그러면 마이그레이션이 구조적으로 재개 가능해진다.
- 에이전트가 지나치게 몸을 사리는 경우가 있는데, "컴파일러가 다음 단계에서 실수를 잡아준다"는 맥락을 프롬프트에 단호하게 박아넣는 게 해법이 됐다.
- 번역기가 확신하지 못하는 부분은
// TODO(port): <이유>로 표시해 다음 단계로 넘긴다. - 리뷰는 서로 다른 컨텍스트를 가진 적대적 리뷰어 2명이 맡고, 둘의 의견이 갈리면 세 번째 에이전트가 판정한다.
- 같은 실수가 여러 파일에서 반복되면 파일별로 고치지 않는다. 룰북에 문장 하나를 추가하고 해당 배치를 재생성한다. 코드를 손으로 패치하는 일은 없다.
컴파일러를 루프 안에 둘지는 언어에 따라 갈린다. TypeScript는 단위 검사가 몇 초면 끝나므로 모든 루프 안에서 돌렸고, Rust는 cargo가 몇 분씩 걸리므로 루프에서 아예 배제하고 다음 단계로 미뤘다.
4~6단계 — 컴파일, 실행, 동작 일치
세 단계 모두 같은 루프 구조를 쓰고 사람의 판단은 갈수록 덜 필요해진다.
컴파일 단계는 오케스트레이터 스크립트가 워크스페이스 전체에 컴파일러를 한 번 돌리고, 나온 에러 목록을 "픽서 에이전트"들이 병렬로 처리하는 식이다. 이때 에러 목록을 사람이 훑어보는 게 의미가 있는데, 구조적 문제를 발견할 수 있기 때문이다. Bun 사례에서는 Zig의 지연 컴파일이 용인하던 순환 임포트를 정리한 뒤 Rust 모듈 에러가 수천 건 쏟아졌고, 어떤 의존성을 삭제·이동·경계 재구성할지 분류하는 로직을 루프에 인코딩하는 방식으로 해결했다.
실행 단계의 기계적 진실은 스모크 테스트에서 나오는 크래시다. 여기서도 개별 대응이 아니라 근본 원인별로 묶어서 적대적 서브에이전트가 검토한다.
마지막 동작 일치 단계에서는 사전 조건 단계에서 만든 테스트 스위트를 샤딩해 돌린다. 눈여겨볼 장치가 빌드 데몬이다. 바이너리를 다시 빌드할 수 있는 프로세스를 이 데몬 하나로 제한하고, 픽서들은 패치만 작성한다. 데몬이 패치를 모아 한 번만 리빌드하고 영향받는 테스트를 재실행해 결과를 돌려준다. 가장 비싼 연산을 여러 에이전트가 각자 트리거하지 못하게 직렬화하는 것이다.
테스트 스위트가 없다면? Python → TypeScript 사례에서는 Claude로 실제 사용 시나리오 7개를 원본과 포팅본 양쪽에 돌려 결과를 diff하는 작은 스크립트를 만들었고, 실패한 시나리오마다 전용 픽서 에이전트를 붙여 7개가 전부 통과할 때까지 돌렸다. 여기서 한 발 더 나가 Claude가 직접 엔드투엔드 테스트 스위트를 설계하고 밤새 자율 실행하며 깨진 것을 고치는 과정을 나흘 연속 반복했다. 시나리오 목록으로는 예측할 수 없던 자잘한 문제들이 이 과정에서 걸렸다. 원문의 결론은 명확하다. 심판을 물려받을 수 없으면 만들게 하면 되고, 어느 쪽이든 원본 코드베이스가 정답이다.
실무 지침
원문이 프로젝트 전반에서 유효했다고 꼽은 것들이다.
- 이 가이드를 그대로 따르지 말 것. 시작점으로 두고 자기 프로젝트에 맞는 계획을 Claude와 먼저 세운다.
- 개별 실패에 매달리지 말 것. 그건 루프가 할 일이고, 사람의 주의는 패턴에 가야 한다.
- 리뷰는 적대적으로, 검증은 기계적으로. 심판은 컴파일러·diff·테스트 스위트가 맡는다.
- 모든 곳에 가장 큰 모델을 쓰지 말 것. 토큰 소비는 루프에 집중되므로 루프를 의도적으로 설계한다. 대량 구현 팬아웃은 작은 모델이 잘 처리하고, 큰 모델은 리뷰어와 "다른 에이전트가 따를 규칙을 작성하는 작업"에 아껴 쓴다.
- 사람의 시간은 앞단에 몰아넣을 것. 룰북과 스트레스 테스트가 가장 오래 걸리고, 그 뒤는 대체로 큐를 소진하는 과정이다.
결과와 트레이드오프
Bun의 Rust 포팅은 현재 프로덕션에 있다. 트레이드오프도 있는데, Rust 코드의 약 4%가 unsafe 블록 안에 있다. 대부분 C/C++ 경계에서의 한 줄짜리 포인터 연산이다.
측정된 개선은 이렇다. 팀 도구로 검출 가능한 메모리 누수는 모두 수정됐고, 빌드 2,000회 반복 벤치마크에서 메모리 사용량이 6,745MB에서 609MB로 떨어졌다. 바이너리 크기는 Linux와 Windows에서 19% 작아졌다. HTTP 서빙과 next build, tsc 같은 실제 워크로드에서 2~5% 빨라졌다.
함께 보면 좋은 것
- 마이그레이션 스타터 킷: https://github.com/anthropics/code-migration-kit-with-claude-code (본문의 프로세스를 일반화한 템플릿이며, 위 두 포팅이 실제로 이 킷으로 진행된 것은 아니다)
- 레거시 현대화·프레임워크 업그레이드용 code-modernization 플러그인
정리
이 글에서 가져갈 것은 "언어 마이그레이션을 다시 견적 내볼 시점"이라는 판단 근거와, 그 판단을 실행으로 옮기는 구체적인 루프 설계다. 에이전트에게 파일을 던져주면 알아서 된다는 얘기가 아니라, 룰북·심판·기계적 큐라는 세 축을 사람이 앞단에서 제대로 만들어두는 것이 전부에 가깝다.
토큰 비용이 결코 작지 않다는 점도 같이 기억해둘 만하다. 다만 비용의 성격이 "4년치 엔지니어링 리소스"에서 "실패하면 브랜치를 지우고 다시 돌리는 실행 비용"으로 바뀌었다는 게 이 사례들의 진짜 변화다.
#Claude Code
#코드마이그레이션
#AI에이전트
#Rust
#TypeScript
아직 댓글이 없습니다.