Turborepo를 Go에서 Rust로 점진적으로 마이그레이션하는 과정에서, run과 prune 명령어 이식을 위해 'Rust-Go-Rust 샌드위치' 아키텍처를 설계했다. Go 코드에서 CGO를 통해 Rust 정적 라이브러리를 호출하고, 두 언어 간 통신에는 protobuf를 사용하는 하이브리드 바이너리 전략이다. 6개 플랫폼(Windows/Linux/macOS, x86-64 + aarch64) 크로스 컴파일 문제는 Zig의 C 컴파일러(zig cc)로 해결했다.
핵심 포인트- 'Go 샌드위치' 전략: Rust 바이너리 -> Go 바이너리 -> Rust 정적 라이브러리(turborepo-ffi) 순서로 호출하는 하이브리드 아키텍처
- CGO로 Go 바이너리에 Rust staticlib을 링크, Go-Rust 간 통신에는 protobuf 채택
- 완전 재작성 대신 점진적 이식: Go build tag로 구현 토글 가능 -> 언제든 Go 구현으로 롤백 가능
- 크로스 컴파일 문제: Windows ARM에서 Go(MinGW)와 Rust(MSVC)의 ABI 비호환 -> x86-64 에뮬레이션으로 우회
- C 의존성 지옥 탈출: zig cc 내장 크로스 컴파일 지원으로 6개 플랫폼 빌드 일원화
- 핵심 교훈: 이식 중에도 계속 기능 출시 -- 완전 재작성은 수개월 릴리스 중단 초래
상세 정리- Turborepo는 기존 두 바이너리로 분리: Go 바이너리(코어 로직)와 Rust 바이너리(CLI 파싱)
- run 명령어: 설정 파싱 -> 패키지 그래프 -> 변경 감지 -> 태스크 그래프 -> 병렬 실행 -> 캐싱 -> 요약
- JSON 직렬화로 그래프 전달: 대형 그래프에서 오버헤드 과대 -> 탈락
- 트램폴린(Go->Rust->Go 콜백) 방식: 인프라 복잡도 과다 -> 탈락
- 채택된 Go 샌드위치: turborepo-ffi를 C staticlib으로 컴파일 -> CGO로 Go 바이너리에 링크
- 통신 포맷으로 protobuf 선택: 플랫폼 독립적, 스키마 명시적, Go/Rust 타입 자동 생성
- 이식 예시: getGlobalHashableEnvVars 함수 -> protobuf I/O 타입 추가 -> ffi.go에서 호출
- Windows ARM: Go(MinGW ABI)와 Rust(MSVC ABI) 비호환 -> x86-64 바이너리 + OS 에뮬레이션 활용
- C 의존성 문제: staticlib는 독립 바이너리가 아니어서 C 의존성(zlib, libunwind 등) 직접 링크 불가
- 크로스 컴파일 C 의존성: 플랫폼별 패키지 매니저 의존으로 자동화 불가
- zig cc: 내장 크로스 컴파일 + 플랫폼별 bundled library(libunwind 포함) 제공
- Go 측 링커 플래그: -linkmode external -extldflags=-static 으로 외부 링커(Zig) 사용
- turbo 1.8.6이 Go 샌드위치 적용 첫 릴리스
- 각 이식 조각은 즉시 프로덕션 투입 -> 미사용 코드로 인한 잠재 버그 방지
왜 읽나Go->Rust 점진적 마이그레이션에서 프로덕션 릴리스를 유지하면서 크로스 컴파일과 언어 간 FFI 문제를 Zig로 해결한 실전 엔지니어링 결정 과정을 상세히 배울 수 있다.