RuboCop의 새로운 플러그인 시스템, Ruby LSP 통합, 그리고 AST 파서의 진화

[JA] RuboCop: Modularity and AST Insights / Koichi ITO @koic

3줄 요약

  • RuboCop은 비공식적인 몽키 패치 방식의 플러그인 시스템에서 LintRoller 기반의 공식적인 시스템으로 전환하여 확장성과 유지보수성을 크게 향상시켰습니다.
  • Ruby LSP와 RuboCop 내장 LSP 기능 간의 통합을 통해 코드 분석 성능을 최적화하고 Ruby 개발 환경의 일관성을 높이는 실험적 시도가 진행 중입니다.
  • Ruby 3.4 이후 `parser-gem`의 유지보수 한계로 인해 RuboCop은 `Prism` 파서를 기본으로 채택하여 최신 Ruby 문법 지원 및 파서 의존성 관리를 개선했습니다.

본 발표는 Ruby의 정적 코드 분석 도구인 RuboCop의 최신 변화를 다룹니다. 특히, RuboCop의 플러그인 시스템 개선, Ruby LSP (Language Server Protocol)와의 통합, 그리고 추상 구문 트리(AST) 파서의 진화에 초점을 맞춰, RuboCop이 직면했던 문제점과 이를 해결하기 위한 새로운 접근 방식들을 상세히 설명합니다. 이는 RuboCop의 유지보수성, 확장성, 그리고 Ruby 개발 생태계 내에서의 통합성을 향상시키려는 노력의 일환입니다.

RuboCop 플러그인 시스템의 혁신

과거 RuboCop은 비공식적인 inject_defaults와 같은 몽키 패치 방식을 통해 플러그인 확장을 지원했습니다. 이 방식은 require를 통해 설정 파일을 주입하는 형태로 동작했으며, 전 세계적으로 복사-붙여넣기(copy-paste) 방식으로 사용되어 코드 중복, 버그 발생, 그리고 유지보수성 저하와 같은 심각한 문제들을 야기했습니다. 특히, LSP와 같은 데몬성 프로세스에서는 동적인 설정 변경 추적이 어려워 불안정성이 더욱 두드러졌습니다.

이를 해결하기 위해 RuboCop 1.7부터는 LintRoller를 활용한 공식적인 플러그인 시스템이 도입되었습니다. 이 새로운 시스템은 제어의 역전(Inversion of Control) 원칙을 적용하여, RuboCop 본체가 플러그인의 설정 정보를 중앙 집중적으로 관리하고 병합하도록 변경되었습니다. 사용자 측면에서는 require 대신 plugins 키워드를 사용하여 플러그인을 로드하게 되며, 기존 방식 사용 시 경고가 발생합니다. 플러그인 개발자는 LintRoller::Plugin을 상속받아 about, supported, rules와 같은 메서드를 구현하고, gemspec의 메타데이터를 통해 플러그인 클래스를 등록함으로써 공식 API를 통해 안전하게 확장 기능을 제공할 수 있게 되었습니다. 이는 코드 중복을 제거하고, 버그 발생 가능성을 줄이며, 전체 시스템의 유지보수성을 크게 향상시키는 중요한 변화입니다.

RuboCop과 Ruby LSP의 통합

최근 Rails 7.1부터 devcontainer에 Ruby LSP가 기본으로 활성화되는 등 LSP의 활용이 보편화되고 있습니다. RuboCop은 자체적으로 내장된 LSP 기능을 가지고 있었지만, ruby-lsp-addon을 통해 Ruby LSP와 통합하려는 시도가 진행 중입니다. 이는 ruby-lsp-addon이 RuboCop의 내장 LSP 런타임을 재사용할 수 있도록 어댑터를 구축하는 방식으로 이루어집니다. 이 통합의 목표는 코드 파싱 결과를 재활용하여 성능을 최적화하고, Ruby LSP 생태계 전반에서 일관된 코드 분석 및 포맷팅 기능을 제공하는 것입니다. 아직 실험적인 단계이지만, 여러 LSP 구현체 간의 중복 작업을 줄이고 협업을 강화하여 Ruby 개발 경험을 개선할 잠재력을 가지고 있습니다.

AST 파서의 진화: parser-gem에서 Prism으로

RuboCop은 오랫동안 parser-gem을 Ruby 코드의 AST 파서로 사용해왔습니다. 그러나 parser-gem은 Ruby 3.4의 블록 파라미터와 같은 최신 문법을 지원하지 않는 등 유지보수가 사실상 중단된 상태입니다. 이로 인해 RuboCop은 최신 Ruby 버전에 대한 호환성 문제를 겪게 되었습니다.

이에 RuboCop 1.62부터는 Prism 파서를 선택적 백엔드로 도입했으며, RuboCop 1.75 이상부터는 Ruby 3.4 이상 환경에서 Prism을 기본 파서로 채택했습니다. Prism은 Ruby 자체에서도 활용되는 파서로, parser-gem 인터페이스를 에뮬레이션하는 트랜슬레이션 레이어를 통해 RuboCop에 통합되었습니다. 이 전환은 parser-gem의 유지보수 부담을 해소하고, 최신 Ruby 문법에 대한 즉각적인 지원을 가능하게 했습니다. 또한, 파싱 처리 속도 향상(약 1.3배)과 에러 톨러런스(Error Tolerance)와 같은 이점도 기대됩니다. 그러나 AST 구조 변경이 RuboCop의 커스텀 Cop 개발자들에게 미칠 막대한 영향 때문에, 현재는 Prism의 네이티브 AST를 직접 사용하는 대신 기존 parser-gem 인터페이스를 유지하는 방식을 채택하고 있습니다. 이는 RuboCop 개발자들의 고민과 함께, AI 기반 코드 어시스트를 통한 자동 마이그레이션과 같은 미래 기술의 필요성을 시사합니다.

RuboCop은 플러그인 시스템의 공식화, Ruby LSP와의 통합, 그리고 `Prism` 파서로의 전환을 통해 지속적으로 발전하고 있습니다. 이러한 변화들은 RuboCop의 내부 구조를 현대화하고, 개발자 경험을 개선하며, 최신 Ruby 버전에 대한 지원을 강화하는 데 기여합니다. 특히 `parser-gem`에서 `Prism`으로의 전환은 RuboCop이 Ruby 언어의 진화에 발맞춰 나가는 중요한 단계이며, 이는 장기적인 유지보수성과 성능 향상을 위한 필수적인 선택입니다. 앞으로 RuboCop은 인터페이스 중심의 설계를 통해 재사용성과 확장성을 더욱 높여, Ruby 개발 생태계 전반에 긍정적인 영향을 미칠 것으로 기대됩니다.