Ruby는 코드를 실행하기 위해 먼저 렉싱(Lexing) 또는 토큰화(Tokenizing) 과정을 거쳐 코드를 의미 있는 최소 단위인 토큰 목록으로 분할합니다. 이어서 파싱(Parsing) 단계에서는 토큰들 간의 문법적 관계를 파악하여 추상 구문 트리(AST: Abstract Syntax Tree)라는 계층적 구조를 구축합니다. 초기 Ruby 버전(1.8까지)은 이 AST를 직접 순회하며 코드를 해석하고 실행하는 트리 워커(Tree Walker) 방식의 인터프리터 모델을 사용했습니다. 이는 구현이 간단하다는 장점이 있었으나, 메모리 상에 데이터가 분산되어 CPU 캐시 효율성이 떨어져 성능이 느리다는 단점이 있었습니다. 강연에서는 간단한 인터프리터를 직접 구현하는 과정을 통해 숫자 및 기본적인 연산을 처리하는 원리를 보여주며, Ruby 1.8의 C 코드 예시를 통해 실제 내부 동작 방식과 &&
(AND) 연산의 단축 평가(short-circuiting)가 어떻게 구현되는지 상세히 설명합니다.
성능 개선을 위해 Ruby 1.9부터는 인터프리터 방식에서 컴파일러와 가상 머신(VM: Virtual Machine)을 사용하는 방식으로 전환되었습니다. 이 모델에서 컴파일러는 AST를 CPU가 처리하기 쉬운 순차적인 명령어 목록인 바이트코드(Bytecode)로 변환합니다. 이 바이트코드는 스택 기반의 VM에 의해 실행되며, 이러한 전환을 통해 Ruby는 평균 2~4배 더 빨라졌습니다. 강연은 바이트코드 생성 과정과 VM이 스택을 활용하여 명령어를 처리하는 방식을 구체적인 예시와 함께 보여주며, ruby -d-dump
명령어를 통해 실제 Ruby의 바이트코드를 직접 확인할 수 있음을 알려줍니다.
최근 Ruby 버전에는 JIT(Just-In-Time) 컴파일러가 도입되어 바이트코드를 머신 코드(Machine Code)로 직접 컴파일함으로써 성능을 더욱 향상시켰습니다. YJIT과 같은 JIT 컴파일러는 Rails 애플리케이션의 속도를 10~30% 개선하며, 이는 C로 작성된 Ruby의 핵심 부분을 순수 Ruby로 재작성할 수 있게 하는 등 ‘더 많은 Ruby’를 가능하게 합니다. 이는 코드의 가독성과 유지보수성을 높여 Ruby 개발 커뮤니티의 기여를 더욱 용이하게 만듭니다.
파서는 컴파일러뿐만 아니라 Rails, IRB, Rubocop, VS Code 확장 등 다양한 Ruby 개발 도구에서 핵심적인 역할을 수행합니다. Ruby 언어에 새로운 문법이 추가될 때마다 각 도구가 자체 파서를 업데이트해야 하는 문제점이 있었으나, Prism이라는 새로운 통합 파서의 도입으로 이러한 파편화된 생태계를 개선하고자 합니다. Prism은 C Ruby의 기본 파서가 되었으며, JRuby, TruffleRuby 등 다른 Ruby 구현체와 여러 젬에서도 활용되어 통일된 파싱 경험을 제공합니다.
VM의 동작 방식을 이해하는 것은 개발자가 코드 성능을 최적화하는 데 필수적입니다. 예를 들어, 0 + 1
과 같은 빈번한 연산에 대해 Ruby VM은 ‘빠른 경로(fast path)’ 최적화를 통해 성능을 향상시킵니다. method_missing
사용, while true
대신 loop
사용, 과도한 메모이제이션(memoization) 등 코드를 작성하는 방식이 VM의 동작에 영향을 미쳐 성능 트레이드오프를 발생시킬 수 있음을 설명하며, 이러한 지식이 더 나은 의사결정을 돕는다고 강조합니다.