Ruby 4.0은 2025년 12월 25일, 루비(Ruby) 탄생 30주년을 맞아 공개된 대형 릴리스입니다. 다만 이번에 메이저 버전이 올라간 것은 파격적인 변경 사항 때문이 아니라, 커뮤니티의 30년 역사를 기념하기 위해서입니다.
사실 루비는 시맨틱 버전 관리(Semantic Versioning)를 엄격하게 따르지 않는 언어입니다. 창시자인 마츠모토 유키히로(마츠, Matz)는 변경 내용이 자신을 감동시킬 때 메이저 버전을 올립니다. 그리고 이번 버전에는 30년의 역사를 기념하며 언어를 한 단계 확장하는 기능들이 담겼습니다.

루비 4로 업그레이드하려는 개발자들에게 반가운 소식은, 업그레이드 과정이 비교적 수월할 것이라는 점입니다. 실험적인 Ruby::Box와 새로운 JIT 컴파일러 ZJIT, 동시성(concurrency) 개선 등 여러 기능이 추가되었지만 대부분 하위 호환성을 유지합니다.
그럼 지금부터 루비 4에서 달라진 점과 원활하게 업그레이드하는 방법을 하나씩 살펴보겠습니다.
Ruby::Box란 무엇인가?
Ruby 4.0에서 가장 흥미로운 실험적 기능 중 하나가 바로 Ruby::Box입니다. 루비 프로세스 내부에 격리된 네임스페이스, 즉 '컨테이너'(Docker 컨테이너와는 다릅니다)를 만들어 주는 기능입니다.
쉽게 말해 루비 프로세스 안에 또 하나의 독립된 루비 세계를 만드는 방식입니다. 새로운 Box를 생성하면 그 안에서 로드된 클래스, 모듈, 전역 변수, 상수, 심지어 C 확장까지 모두 해당 Box 안에 갇히게 됩니다.
마치 언어 수준의 경량 가상화처럼, 각 Box는 자신만의 상태를 가지며 정의가 다른 Box나 메인 환경으로 새어 나가지 않습니다.
다만 아직 실험 단계이기 때문에 불안정한 부분이 있을 수 있고, 성능 오버헤드도 고려해야 합니다. 격리에는 비용이 따르기 때문에 코어 팀은 의도적으로 옵트인(opt-in) 방식으로 설계했습니다.
Ruby 4.0 시점의 Box는 진정한 병렬 실행을 위한 것이 아니라, 더 스마트한 코드 로딩의 기반을 다지는 역할입니다. 앞으로 어떻게 발전할지 기대되는 부분입니다. Box를 사용하려면 먼저 환경 변수를 활성화해야 합니다:
RUBY_BOX=1
ZJIT가 중요한 이유
Ruby 4.0에는 YJIT의 후속작으로 개발된 완전히 새로운 JIT(Just-In-Time) 컴파일러, ZJIT가 도입되었습니다. 놀랍게도 MRI는 이제 두 개의 JIT 컴파일러를 갖추게 된 셈입니다. ZJIT는 YJIT와 다른 방향으로 개발되고 있으며, 두 프로젝트는 병행됩니다. 이미 YJIT를 사용 중이라면 굳이 전환할 필요는 없습니다.
YJIT 간단 복습
YJIT('Yet Another JIT')는 Shopify가 개발해 Ruby 3.1에 도입된 JIT 컴파일러입니다. Lazy Basic Block Versioning 방식을 사용해 코드의 작은 덩어리(기본 블록)를 즉석에서 컴파일하고, 런타임 타입에 맞게 최적화합니다.
YJIT는 많은 루비 애플리케이션의 성능을 크게 끌어올렸고, 도입도 비교적 쉬웠습니다. Rust로 작성되었으며 최근 루비 버전에서 기본/주력 JIT로 자리 잡았습니다.
ZJIT는 무엇이 다른가
ZJIT는 좀 더 전통적인 메서드 기반 JIT 전략을 취합니다. 작은 블록을 조각조각 컴파일하는 대신, SSA(Static Single Assignment) 중간 표현과 일반적인 컴파일러 파이프라인을 사용해 메서드 전체나 더 큰 코드 단위를 한 번에 컴파일합니다.
'교과서적인' JIT 컴파일러에 가까운 구조라, 기여자들이 이해하고 개선하기가 훨씬 쉬워질 것으로 기대됩니다.
루비 코어 팀은 ZJIT의 목표를 두 가지로 명확히 밝혔습니다. 첫째는 YJIT보다 고도화된 최적화를 통해 루비의 장기적인 성능 상한선을 끌어올리는 것이고, 둘째는 커뮤니티가 JIT를 더 쉽게 해킹하고 기여할 수 있도록 만드는 것입니다.
Ruby 4.0에서 ZJIT 활성화하기
ZJIT는 Ruby 4.0에 포함되어 있지만 기본적으로는 비활성화되어 있습니다. 사용해 보려면 시스템에 Rust 1.85 이상이 설치된 상태로 루비를 빌드해야 합니다. JIT 코드 자체는 루비 바이너리에 포함되지만, 빌드 과정에서 Rust가 필요합니다. 이후 --zjit 플래그와 함께 루비를 실행하면 됩니다.
JIT를 기본값으로 두면 계속 YJIT의 혜택(역시 꾸준히 개선되고 있는)을 누릴 수 있습니다. 참고로 --rjit 플래그는 이번 릴리스에서 제거되었으니 유의하세요.
Ruby 4의 Ractor 개선 사항
루비 3.0에서 도입된 Ractor는 병렬 처리를 위한 실험적 기능입니다. Ractor는 단일 프로세스 안에서 여러 루비 인터프리터를 실행해, 한 번에 하나의 스레드만 허용하는 GIL(Global Interpreter Lock)의 제약을 우회할 수 있게 해 줍니다. 아직 널리 쓰이지는 않지만, 지속적인 투자는 루비의 미래에서 중요한 위치를 차지하고 있다는 분명한 신호입니다.
Ruby 4.0에서도 Ractor는 여전히 '실험적'으로 표시되어 있지만, 실용성에 한 걸음 더 가까워지도록 큰 개선과 API 변경이 이루어졌습니다.
Ractor를 써 본 개발자라면 메시지를 주고받는 과정이 다소 번거로웠음을 알 것입니다. Ruby 4.0은 기존 API를 더 견고한 Ractor::Port 메커니즘으로 교체했습니다. Port는 Ractor들이 값을 주고받을 때 사용하는 일종의 파이프 또는 채널입니다. 이제 각 Ractor는 기본 포트(Ractor.current.default_port)를 갖고, 커스텀 Port 객체를 만들어 전달할 수도 있습니다.
이번 변경에는 일부 호환성을 깨는(breaking) 수정도 포함되어 있습니다. 가장 두드러진 것은 Ractor.yield와 Ractor#take가 제거된 점입니다.
내부적으로도 Ruby 4.0의 Ractor 구현은 성능과 안전성을 위해 조정되었습니다. Ractor 간 공유 상태를 줄여 격리가 실수로 깨질 가능성을 낮추고, 멀티코어 시스템에서 CPU 캐시 활용도를 높였습니다.
덕분에 Ractor는 더 잘 확장되고 더 빠르게 동작하지만, 여전히 스레드나 프로세스만큼 널리 쓰이지는 않고 있습니다.
Ruby 4의 *nil 동작 변화
Ruby 4.0에서는 nil을 스플랫(splat)할 때 더 이상 nil.to_a가 호출되지 않습니다.
기존 루비 버전에서는 arr = [*nil]처럼 작성하면 내부적으로 nil.to_a(빈 배열 반환)가 호출되어 빈 배열을 얻었습니다. 다소 마법 같고 일관성 없는 동작이었죠. 왜 *nil이 빈 배열처럼 행동해야 할까요?
Ruby 4.0에서는 이런 애매한 동작이 사라졌습니다. nil에 스플랫(*)을 적용하면 to_a가 호출되지 않고, '스플랫할 것이 없다'로 취급됩니다. 이는 Ruby 3.4에서 도입된, 더블 스플랫 **nil이 nil.to_hash를 호출하지 않는 방식과도 일관됩니다.
줄 시작 위치의 논리 연산자 지원
많은 루비스트를 미소 짓게 만들 작은 문법 개선입니다!
이제 &&, ||, and, or를 줄의 '앞'에 배치해 이전 줄의 불리언 표현식을 이어갈 수 있습니다. 예를 들어 다음과 같이 작성할 수 있습니다:
if user_signed_in?
&& user.admin?
&& feature_enabled?
perform_admin_task
end
기존에는 아래와 같이 연산자를 줄 끝에 두어야 했습니다:
if user_signed_in? &&
user.admin? &&
feature_enabled?
perform_admin_task
end
원래 처음부터 동일하게 동작했어야 할 부분이라, 이번 업데이트는 반가운 소식입니다.
Ruby 4의 핵심 클래스 업데이트
Ruby 4.0에는 코어 클래스에 대한 소소하지만 유용한 변화들도 함께 포함되어 있습니다.
먼저 Set 클래스가 이제 빌트인 코어 클래스가 되어, require 'set' 없이 바로 사용할 수 있습니다. 이와 함께 set/sorted_set.rb는 제거되었습니다.
Pathname도 코어 클래스로 승격되어 동일하게 require 없이 사용 가능합니다.
Array에도 새로운 변화가 있습니다! 이번 릴리스는 Array#find의 성능을 개선해 단순 선형 탐색보다 더 똑똑하게 배열을 검색합니다. 또한 Array#rfind가 새로 도입되었습니다. 오타가 아닙니다! 이 메서드는 배열에서 조건에 맞는 '마지막 요소'를 찾아줍니다.
이 밖에도 새로운 수학 메서드, 인트로스펙션(introspection) 제어, Enumerator 개선 등이 추가되었습니다. 전체 개선 목록은 공식 문서를 꼭 확인해 보세요.
Ruby 4로 업그레이드하는 방법
운영 중인 앱의 루비 버전을 올리는 작업은 언제나 신중해야 하지만, Ruby 3.4에서 업그레이드한다면 지금껏 경험한 업그레이드 중 손꼽힐 만큼 수월한 편입니다.
안전한 전환을 위한 팁을 정리했습니다.
공식 릴리스 노트 확인
무엇보다 공식 릴리스 노트를 먼저 읽으세요. 공식 폐기(deprecation) 공지가 있는지 재차 확인하세요. 놀랄 일은 없을 것입니다. Ruby 3.4에서 예정된 폐기 항목에 대해 이미 경고를 받았을 테니까요.
폐기 경고 처리
만약 인터프리터의 폐기 경고를 무시해 왔다면, 언어 버전을 올리기 전에 반드시 먼저 처리하세요.
기준 테스트 확보
다음으로 좋은 테스트 코드를 갖추세요. 앱의 동작이 바뀌지 않았다는 확신을 얻는 데 큰 도움이 됩니다. 테스트가 전혀 없다면, 최소한 핵심 경로라도 커버할 수 있도록 지금이 투자하기 좋은 시점입니다.
Bundler 업데이트
그다음 Bundler를 최신 버전으로 업데이트하는 것이 좋습니다. 최신 버전으로 올린 후 bundle install을 실행하고 에러나 경고가 없는지 확인하세요.
루비 버전 변경
이제 새 루비 버전을 설치하고 전환합니다. rbenv나 asdf 같은 루비 버전 매니저가 로컬 버전 관리에 널리 쓰입니다. 앱이 Docker에서 돌아간다면 Dockerfile의 루비 버전을, Docker 없이 플랫폼에서 돌아간다면 해당 플랫폼에서 루비 버전을 업데이트하세요.
Gemfile의 Ruby 버전 업그레이드
Gemfile에서 ruby 디렉티브로 버전을 고정했다면 이를 업데이트하고, 마지막으로 한 번 더 bundle install을 실행합니다.
테스트 실행
마지막으로 테스트를 실행하세요! 문제가 없다면 앱의 주요 경로도 직접 돌려보며 확신을 더하세요. 업그레이드를 배포하기 전에 Honeybadger 같은 예외 모니터링 서비스를 활용하면, Ruby 4 업그레이드가 사용자에게 문제를 일으키는지 실제로 '알 수' 있습니다.
이 단계를 따르면 루비가 제공하는 최고의 기능을 비교적 고통 없이 계속 누릴 수 있을 것입니다. 테스트를 실행할 때, 루비의 30번째 생일 축하 인사도 잊지 마세요!