-
Bundler의 성능 한계를 분석하며, Python의 패키지 관리자 uv가 빠른 이유를 비교
- uv의 속도는 Rust 언어 때문이 아니라 병렬 다운로드, 글로벌 캐시, 메타데이터 기반 의존성 처리 등 구조적 설계 덕분임
- Bundler는 다운로드와 설치 과정이 결합되어 있어 병렬 처리에 제약이 있으며, 이를 분리하면 큰 개선 가능
-
글로벌 캐시 통합, 하드링크 설치, PubGrub 해석기 통합 등으로 RubyGems와 Bundler의 중복을 줄일 수 있음
- 언어 재작성 없이도 대부분의 성능 향상은 Ruby 코드 내에서 달성 가능, uv 수준의 속도에 근접할 수 있음
Bundler와 uv의 성능 비교
- RailsWorld에서 제기된 “왜 Bundler는 uv만큼 빠르지 않은가?”라는 질문을 계기로 Bundler의 성능 병목을 조사
- 작성자는 Bundler가 uv 수준의 속도를 달성할 수 있다고 확신하며, 성능 차이는 언어가 아니라 설계의 문제라고 명시
- Andrew Nesbitt의 글 *“How uv got so fast”*를 인용해 uv의 핵심 최적화 방식을 Bundler에 적용 가능 여부로 분석
Rust로의 재작성 여부
- uv가 Rust로 작성된 점은 사실이나, 속도의 본질적 원인은 Rust 자체가 아님
- Bundler의 병목을 제거해 “Rust로 다시 쓰는 것만이 남은 개선책”이 된다면 그것이 성공이라 평가
- Rust 재작성은 기존 호환성 제약 없이 실험적 설계를 시도할 자유를 제공하지만, 필수 조건은 아님
Bundler의 구조적 병목
- Bundler는 gem 다운로드와 설치를 하나의 메서드에 결합해 병렬 다운로드가 불가능
- 예시 코드에서 install 메서드가 fetch_gem_if_not_cached와 install을 연속 실행
- 이로 인해 의존 관계가 있는 gem(a -> b -> c)은 순차적으로만 설치됨
- 실험 결과, 의존성이 있는 경우 9초 이상 소요되지만, 독립적인 gem(d, e, f)은 병렬 다운로드로 4초 내 완료
-
다운로드와 설치를 분리하면 의존성 규칙을 유지하면서도 병렬 처리 가능
- 네 단계(다운로드 → 압축 해제 → 컴파일 → 설치)로 분리 제안
- 순수 Ruby gem은 의존성 설치 순서를 완화해 추가 속도 향상 가능
캐시 및 설치 최적화
- uv의 글로벌 캐시와 하드링크 설치 방식을 Bundler에도 적용 가능
- Bundler와 RubyGems는 현재 Ruby 버전별로 별도 캐시를 사용
-
$XDG_CACHE_HOME 기반의 공유 캐시로 통합 필요
- 하드링크 설치는 캐시 통합 후 적용 가능
- Bundler는 이미 PubGrub 의존성 해석기를 사용하지만, RubyGems는 여전히 molinillo를 사용
- 두 시스템의 해석기 통합이 기술 부채 해소의 핵심
Rust 관련 최적화 요소의 적용 가능성
-
Zero-copy 역직렬화는 RubyGems의 YAML 파싱 단계에서 일부 적용 가능성
-
Ruby의 GVL(Global VM Lock) 은 IO 중심 작업에서는 병렬 처리에 큰 제약이 없음
- IO와 ZLIB 처리는 GVL을 해제하므로 병렬 실행 가능
- 단, 작은 파일 쓰기에서는 GVL 관리 오버헤드가 성능 저하 요인
- Ruby 내부에서 이를 개선하는 작업이 진행 중
-
버전 비교 최적화: uv는 버전을 u64 정수로 인코딩해 비교 속도를 높임
- Ruby에서도 Gem::Version을 정수 기반으로 변환해 해석기 성능 향상 가능
- 이미 관련 리팩터링 시도가 있었으나 하위 호환성 문제로 보류
결론 및 향후 계획
- uv의 속도는 언어보다 불필요한 작업을 제거한 설계 덕분이며, Bundler도 같은 방향으로 개선 가능
- RubyGems와 Bundler는 이미 현대적 패키지 관리 구조를 갖추고 있어, uv 수준의 속도 달성이 현실적
- 가장 큰 과제는 레거시 코드와 호환성 유지
- Rust로 재작성하지 않아도 99%의 성능 향상은 Ruby 코드 내에서 가능, 나머지 1%는 미미한 수준
- 후속 글에서는 Bundler와 RubyGems의 실제 프로파일링과 구체적 병목 원인을 다룰 예정