RBS와 Sorbet의 통합 필요성
- RBS (Ruby Signature): Ruby 코드의 변수, 인스턴스 변수, 메서드 반환 값, 파라미터 등의 타입을 기술하는 표준 표기법입니다. 초기에는 별도의
.rbs
파일에 작성되었으나, 이후 코드 내 주석으로 타입을 정의하는 인라인 RBS가 도입되어 개발 편의성이 향상되었습니다. - Sorbet: Shopify에서 사용하는 C++로 작성된 고성능 정적 타입 체커입니다. 약 9백만 라인의 코드를 1분 이내에 타입 검사할 정도로 빠르고 메모리 효율적입니다. 그러나 Sorbet은
sorbet-runtime
Gem을 통한 SIG 문법에 의존하여 런타임 시 메서드 호출이 발생하고, 이는 라이브러리 의존성 문제를 야기했습니다.
순수 C 라이브러리로의 마이그레이션 과정
RBS 파서는 다음과 같은 단계를 거쳐 발전했습니다.
- 2022년 이전: 순수 Ruby Gem으로, Ruby로 작성된 내부 파서를 가졌습니다.
- 2022년: Sutaro에 의해 C 확장으로 내부 구현이 재작성되었습니다. 여전히 Ruby API를 사용하지만, 백그라운드에서 C 확장이 동작했습니다. 하지만 이 C 코드는 Ruby VM 기능을 여전히 많이 사용했습니다.
- 현재 (Shopify의 작업): Ruby 확장 API를 전혀 사용하지 않는 순수 C 파서로 전환되었습니다. 이는 기존 Ruby LSP나 Steep 같은 도구는 Ruby Gem처럼 RBS를 계속 사용할 수 있게 하면서도, Sorbet(C++)이나 JRuby와 같은 대체 Ruby 런타임이 C 코드를 직접 호출하여 활용할 수 있도록 했습니다.
Ruby VM 의존성 제거의 이점
- 병렬 처리 가능: Ruby VM의 전역 VM 잠금(Global VM Lock)을 회피하여 다중 스레드 환경에서 RBS 파싱의 CPU 병렬 처리를 가능하게 합니다. Sorbet의 빠른 타입 검사 성능 유지에 필수적입니다.
- 이식성 향상: MRI의 C 확장 API에 대한 비호환성을 제거하여 JRuby와 같은 대체 Ruby 런타임에서도 RBS를 사용할 수 있게 합니다.
- 성능 및 메모리 제어: C 코드에서 메모리 레이아웃 및 사용량을 더욱 세밀하게 제어하여 성능 최적화를 달성할 수 있습니다.
마이그레이션 과정의 주요 도전과 해결책
1. 오류 처리 (Error Handling)
- 문제점: Ruby는 예외(Exception)를 통한 오류 처리가 용이하며,
raise
를 통해 호출 스택의 여러 단계를 건너뛸 수 있습니다. 그러나 C는 예외를 지원하지 않으며, Ruby C API의rb_raise
와 같은 기능은 Ruby VM에 의존적입니다. - 해결책: 함수가 성공 여부를
bool
타입으로 반환하고, 실제 결과 값은out
파라미터를 통해 전달하는 방식으로 변경했습니다. 이는 오류 전파를 수동으로 처리해야 하는 번거로움이 있지만, C 언어에서 일반적인 패턴입니다.
2. 메모리 관리 (Memory Management)
- 문제점: Ruby VM은 가비지 컬렉터(GC)를 통해 동적 메모리 할당 및 해제를 자동 처리합니다. C의
malloc
과free
는 객체 수명 주기가 너무 세분화되어 있어, 복잡한 파싱 과정에서 메모리 누수나 이중 해제(double free) 같은 오류가 발생하기 쉽습니다. 특히 IDE에서 실시간으로 코드를 파싱할 때 부분적인 입력으로 인한 잦은 오류가 메모리 누수로 이어질 수 있습니다. - 해결책: 아레나 할당(Arena Allocation) 또는 슬랩 할당(Slab Allocation) 방식을 도입했습니다. 이는 유사한 수명 주기를 가진 객체들을 하나의 큰 메모리 덩어리(아레나)에 할당하고, 작업 완료 시 아레나 전체를 한 번에 해제하는 방식입니다. RBS 파서는 매우 짧은 수명 주기(밀리초 단위)를 가지므로, 아레나 할당은 개별 객체 해제의 복잡성을 제거하고 메모리 누수 위험을 줄여줍니다. 단, 할당을 수행하는 모든 함수가 할당자 인스턴스를 인자로 받아야 하는 제약이 있습니다.
3. 기타 과제
- 표준 라이브러리 대체: Ruby 표준 라이브러리의 편리한 데이터 구조(Array, Hash 등)를 C에서 대체할 필요가 있었습니다. 잘 테스트되고 고성능의 C/C++ 컨테이너 라이브러리 사용이 권장됩니다.
- 다형성(Polymorphism): Ruby의 객체 지향적 다형성(동적 디스패치)을 C에서 직접 구현하거나 다른 방식으로 해결해야 했습니다.