Sorbet 타입 구문의 과거, 현재, 그리고 미래

Past, Present, and Future of Sorbet Type Syntax – Jake Zimmerman

3줄 요약

  • Sorbet의 `sig` 구문은 Ruby의 동적 특성 및 Stripe의 런타임/정적 타입 검사 요구사항을 충족시키기 위해 선택된 DSL 방식입니다.
  • 트랜스파일, 헤더 파일, 주석 등 다른 접근 방식은 Ruby 호환성, 런타임 검사 부재 등 한계가 있었습니다.
  • "표현식으로서의 타입"이라는 제약과 발전 과정의 문제점들이 있었으나, RBS 주석을 Ruby VM이 직접 파싱하는 등의 미래 개선 가능성이 논의되고 있습니다.

본 글은 Sorbet 타입 검사기의 핵심 요소인 타입 구문(`sig`)의 설계 결정 과정과 미래 전망에 대해 자세히 설명합니다. 많은 사용자들에게 Sorbet의 구문이 보기 좋지 않다는 비판이 있지만, 저자는 언어 설계에서 구문보다 의미론이 훨씬 중요함을 강조하며, Sorbet의 구문이 탄생하게 된 역사적 배경, 목표, 제약 사항들을 공유하고자 합니다. 특히 Stripe 내부에서 발생한 정적 타입 검사에 대한 높은 수요와 기존 런타임 타입 검사 도구의 발전 과정이 Sorbet의 구문 설계에 결정적인 영향을 미쳤음을 밝힙니다.

Stripe는 2017년 초 엔지니어 설문 조사에서 기술 문서 개선 다음으로 Ruby 정적 타입 검사를 최우선 과제로 꼽을 만큼 타입 검사에 대한 요구가 높았습니다. 이미 2013년부터 Odin::Model과 같은 런타임 타입 유효성 검사 도구와 Chalk::Interface와 같은 인터페이스 정의 도구를 사용하고 있었으며, 2016년 말에는 메소드에 런타임 타입 검사를 추가하는 declare_method 라이브러리가 개발되었습니다. 이는 sig 구문의 직접적인 전신으로, 런타임 검사의 중요성이 초기부터 강조되었음을 시사합니다.

새로운 Ruby 타입 검사기를 개발하기로 결정한 팀은 여러 구문 설계 방식을 검토했습니다. 첫째, TypeScript 방식처럼 별도 구문을 만들고 트랜스파일하는 것은 Ruby 호환성을 크게 해치고 빌드 단계를 강제하여 개발 워크플로우를 방해하며 기존 개발 도구와의 통합을 어렵게 한다는 단점이 있었습니다. 둘째, RBS와 같은 헤더 파일 방식은 소스 코드를 변경하지 않아 좋지만, 메소드 본문 내에서의 명시적인 타입 캐스팅이나 런타임 검사를 지원하기 어렵다는 한계가 있습니다. 셋째, JSDoc 방식처럼 주석 안에 타입을 정의하는 것은 Ruby의 동적 특성에서 오는 강력한 장점인 런타임 타입 검사 기능을 포기해야 합니다. 특히 Hyrum’s Law와 같이 API 사용자들이 약속된 계약 외의 동작에도 의존하는 현실에서 런타임 검사는 코드 변경 시 안전성을 보장하는 중요한 수단입니다.

결과적으로 Sorbet은 Stripe의 기존 declare_method DSL을 재활용하는 방식을 선택했습니다. 이는 기존에 존재하던 수많은 어노테이션을 즉시 활용할 수 있었고, Ruby의 동적 특성을 활용하여 메소드 정의 바로 위에 타입 시그니처를 붙여 런타임 검사를 쉽게 구현할 수 있게 했습니다. 이는 Python의 타입 힌트(__annotations__ 속성을 통해 런타임에 접근 가능)와 유사한 의미론적 특성을 가집니다. 즉, 타입 어노테이션이 선택 사항이고, 실행 가능한 구문이며, 정적 및 런타임 검사를 모두 지원할 수 있고, 런타임에 리플렉션이 가능하다는 공통점이 있습니다.

그러나 “표현식으로서의 타입”이라는 접근 방식은 몇 가지 제약을 가져왔습니다. 첫째, | (유니온), & (인터섹션), [] (제네릭)와 같이 타입에 사용하고 싶은 구문이 이미 Module이나 Array 클래스에 정의되어 있어 직접 사용하기 어렵다는 문제가 있습니다(메소드 몽키 패치가 필요하고 충돌 가능성이 있음). 둘째, 타입 구문이 로딩 시점에 평가되면서 순환 참조나 선언 순서 문제(Forward References)를 유발했습니다. 이를 해결하기 위해 sig 구문을 블록 형태로 변경하여 타입 평가를 메소드 첫 호출 시점으로 지연시켰습니다(이는 Python의 from __future__ import annotations와 유사한 해결책입니다). 이러한 제약들은 Integer?(Integer) -> String와 같은 간결한 맞춤형 구문 사용을 어렵게 만듭니다.

Sorbet의 타입 구문 설계는 Ruby 호환성 유지, 런타임 검사 지원, "표현식으로서의 타입"이라는 제약 등 다양한 요소의 영향을 받았습니다. 현재 구문 모델 내에서도 선택적 몽키 패치 지원, 튜플이나 제네릭 메소드와 같은 복잡한 타입의 구문 간결화 등의 개선 여지는 남아 있습니다. 그러나 더 근본적인 개선을 위해 저자는 Soutaro가 제안한 RBS 주석(`#: (Integer) -> String`)을 Ruby VM이 직접 파싱하여 런타임에 노출하는 방식을 제안합니다. 이 방식은 간결한 구문을 사용하면서도 "표현식으로서의 타입" 제약을 벗어나고, 선언 순서 문제를 해결하며, Sorbet의 런타임 검사 기능을 유지할 수 있습니다. 또한, IRB의 자동 완성, JSON 스키마 생성, 린터 규칙 등 다양한 도구 및 라이브러리에서 이 정보를 활용할 수 있게 됩니다. 비록 `T.let`과 같은 인라인 타입 단언에는 적용되지 않지만, 이는 Ruby 타입 생태계 전반에 큰 발전 가능성을 제시합니다. Sorbet의 구문은 고정된 것이 아니며, Ruby 자체의 타입 어노테이션 발전과 함께 계속 개선될 수 있음을 강조하며 글을 마무리합니다.