C 확장부터 순수 C까지: RBS 라이브러리 마이그레이션 이야기

[EN] From C extension to pure C: Migrating RBS / Alexander Momchilov @amomchilov

작성자
jeff
발행일
2025년 05월 27일

핵심 요약

  • 1 Shopify는 Ruby 개발 경험 향상을 위해 RBS(Ruby Signature)를 C 확장 Ruby Gem에서 순수 C 라이브러리로 마이그레이션했습니다.
  • 2 이번 마이그레이션으로 Sorbet과 같은 타입 체커가 Ruby VM 종속성 없이 RBS를 활용할 수 있게 되어 성능과 이식성이 크게 향상되었습니다.
  • 3 주요 기술적 과제는 Ruby의 예외 처리와 가비지 컬렉션을 C의 수동 오류 전파 및 아레나 할당으로 대체하는 것이었습니다.

도입

본 발표는 Ruby 코드의 타입 정의를 위한 표준 표기법인 RBS(Ruby Signature)와 Shopify의 정적 타입 체커인 Sorbet의 통합에 대한 여정을 다룹니다. 기존 Sorbet의 SIG 문법은 런타임 의존성을 강제하고 너무 장황하다는 문제가 있었습니다. 이에 Shopify 개발팀은 Sorbet의 강력한 성능을 유지하면서 RBS의 간결한 문법을 활용하기 위해 RBS를 순수 C 라이브러리로 전환하는 프로젝트를 진행했습니다. 이 발표는 그 과정에서 마주한 기술적 도전과 해결책을 상세히 설명합니다.

RBS와 Sorbet의 통합 필요성

  • RBS (Ruby Signature): Ruby 코드의 변수, 인스턴스 변수, 메서드 반환 값, 파라미터 등의 타입을 기술하는 표준 표기법입니다. 초기에는 별도의 .rbs 파일에 작성되었으나, 이후 코드 내 주석으로 타입을 정의하는 인라인 RBS가 도입되어 개발 편의성이 향상되었습니다.
  • Sorbet: Shopify에서 사용하는 C++로 작성된 고성능 정적 타입 체커입니다. 약 9백만 라인의 코드를 1분 이내에 타입 검사할 정도로 빠르고 메모리 효율적입니다. 그러나 Sorbet은 sorbet-runtime Gem을 통한 SIG 문법에 의존하여 런타임 시 메서드 호출이 발생하고, 이는 라이브러리 의존성 문제를 야기했습니다.

순수 C 라이브러리로의 마이그레이션 과정

RBS 파서는 다음과 같은 단계를 거쳐 발전했습니다.

  1. 2022년 이전: 순수 Ruby Gem으로, Ruby로 작성된 내부 파서를 가졌습니다.
  2. 2022년: Sutaro에 의해 C 확장으로 내부 구현이 재작성되었습니다. 여전히 Ruby API를 사용하지만, 백그라운드에서 C 확장이 동작했습니다. 하지만 이 C 코드는 Ruby VM 기능을 여전히 많이 사용했습니다.
  3. 현재 (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의 mallocfree는 객체 수명 주기가 너무 세분화되어 있어, 복잡한 파싱 과정에서 메모리 누수나 이중 해제(double free) 같은 오류가 발생하기 쉽습니다. 특히 IDE에서 실시간으로 코드를 파싱할 때 부분적인 입력으로 인한 잦은 오류가 메모리 누수로 이어질 수 있습니다.
  • 해결책: 아레나 할당(Arena Allocation) 또는 슬랩 할당(Slab Allocation) 방식을 도입했습니다. 이는 유사한 수명 주기를 가진 객체들을 하나의 큰 메모리 덩어리(아레나)에 할당하고, 작업 완료 시 아레나 전체를 한 번에 해제하는 방식입니다. RBS 파서는 매우 짧은 수명 주기(밀리초 단위)를 가지므로, 아레나 할당은 개별 객체 해제의 복잡성을 제거하고 메모리 누수 위험을 줄여줍니다. 단, 할당을 수행하는 모든 함수가 할당자 인스턴스를 인자로 받아야 하는 제약이 있습니다.

3. 기타 과제

  • 표준 라이브러리 대체: Ruby 표준 라이브러리의 편리한 데이터 구조(Array, Hash 등)를 C에서 대체할 필요가 있었습니다. 잘 테스트되고 고성능의 C/C++ 컨테이너 라이브러리 사용이 권장됩니다.
  • 다형성(Polymorphism): Ruby의 객체 지향적 다형성(동적 디스패치)을 C에서 직접 구현하거나 다른 방식으로 해결해야 했습니다.

결론

RBS를 순수 C 라이브러리로 마이그레이션하는 작업은 Ruby 타입 검사 생태계에 중요한 진전을 가져왔습니다. 이는 Sorbet과 같은 고성능 타입 체커가 Ruby VM의 제약 없이 RBS를 효율적으로 활용할 수 있게 하며, JRuby와 같은 다른 Ruby 런타임에서도 RBS를 사용할 수 있는 길을 열었습니다. 이러한 복잡한 시스템을 개발할 때는 일관된 오류 처리 패턴을 초기에 확립하고, 객체 수명 주기를 신중하게 설계하며, 검증된 외부 라이브러리를 적극적으로 활용하는 것이 중요합니다. 이 프로젝트는 Ruby 개발 경험을 향상시키기 위한 Shopify의 지속적인 노력을 보여줍니다.

댓글 0

댓글 작성

0/1000
정중하고 건설적인 댓글을 작성해 주세요.

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!