주요 컨텐츠로 이동
플랫폼

Metals v2 오픈소스 공개: 수백만 줄의 코드베이스를 위한 Databricks의 Java 및 Scala 언어 서버

코드 인텔리전스 격차를 줄여 2,600만 라인 규모의 Bazel 모노레포에서 Cursor를 효과적으로 작동하게 만든 방법

작성자: Ólafur Páll Geirsson, Iulian Dragos , Alessandro Patti

  • Metals v2는 기존 JVM 언어 서버의 한계를 넘어선 대규모 모노레포를 위해 코드 인텔리전스를 재구상합니다.
  • Metals는 수년 동안 공식 Scala 언어 서버로 사용되어 왔습니다. v2에서 완전한 Java 지원이 제공됨에 따라, 업계에서 가장 큰 규모의 Java 코드베이스를 보유한 일부 기업들이 이제 Java 전용으로 Metals를 시범 도입하고 있습니다.
  • AI는 LSP(Language Server Protocol) 기능의 상대적 가치를 변화시키고 있습니다. 엔지니어가 직접 작성하는 코드가 줄어듦에 따라, 광범위한 자동 완성 및 리팩터링 지원 범위보다 낮은 설정 비용, 빠른 피드백, 신뢰할 수 있는 코드베이스 파악 능력이 더 중요해지고 있습니다.

현재 Databricks의 대부분의 코드는 에이전트가 작성하고 있습니다. 엔지니어가 직접 코딩해야 하는 순간에는, 빠르게 실행되고 최소한의 설정으로 코드를 탐색할 수 있는 가벼운 에디터를 찾습니다. 하지만 Scala와 Java의 경우, 오랫동안 IntelliJ가 표준으로 자리 잡고 있었습니다. 저희 모노레포 규모에서는 사실상 이를 감당할 수 있는 유일한 에디터였습니다. 이 글에서는 널리 사용되는 Scala 언어 서버인 Metals를 확장하여 뛰어난 Java 지원 기능을 갖추고 모노레포 규모에 맞게 확장할 수 있는 대안 솔루션을 구축한 방법을 공유합니다. 업스트림 Metals 팀과의 협업을 통해 이제 Metals v2를 오픈 소스로 공개하여, 대규모 Java 및 Scala 코드베이스를 사용하는 누구나 코딩 에이전트를 가벼운 에디터와 결합하여 사용할 수 있도록 했습니다.

Metals v2는 현재 Cursor, VS Code, Neovim에서 사용할 수 있으며, 설치 방법은 Metals 웹사이트에서 확인할 수 있습니다.

IDE 플라이휠 구축하기

2025년 5월, 저희는 Databricks의 일상적인 에디터 워크플로우를 Cursor 중심으로 표준화하기 시작했습니다. Cursor와 VS Code는 이미 Databricks에서 프론트엔드 및 기타 비 JVM 작업에 널리 사용되고 있었으며, 두 에디터 모두 클라우드 기반 개발 환경을 위한 강력한 SSH 원격 지원을 제공했습니다. 하지만 저희 서비스의 대부분은 Scala와 Java로 작성되어 있었고, 모노레포 규모에서 이를 탐색하는 것이 마지막 걸림돌이자 저희가 해결하고자 했던 문제였습니다.

하나의 에디터로 표준화하는 것은 개인의 선호도를 넘어 중요한 의미가 있었습니다. 통합된 IDE 플랫폼은 플라이휠을 만듭니다. 팀들은 코드 탐색과 개발을 위한 하나의 기준을 공유하고, 플랫폼 팀은 한 곳에 투자를 집중할 수 있습니다. Cursor는 저희 모노레포에서 JVM 작업을 위한 기본 에디터가 되었으며, 충분히 통합되어 올해 IntelliJ 라이선스의 대부분을 갱신하지 않았습니다.

image2.png
그림 1: 도입이 광범위해질수록 플랫폼 투자의 레버리지가 증가하여 공유 IDE가 개선되고 도입이 더욱 강화됩니다.

Cursor로의 전환이 얼마나 진행되었는지는 세 가지 신호로 알 수 있습니다. 전체 IDE 사용량, Metals가 크리티컬 패스에 위치한 Scala 및 Java 파일 열기 이벤트, 그리고 Databricks 외부에서의 도입입니다.

IDE 사용량

가장 광범위한 신호는 전체 IDE 사용량입니다. Databricks에서 Cursor 도입은 자체적으로 성장했으나 2025년 9월에 정체되어 몇 달 동안 유지되었습니다. Metals v2 출시와 함께 성장이 재개되었으며, 2026년 7월까지 주간 활성 IDE 사용자의 92%가 Cursor를 열었으며, IntelliJ는 12%에 그쳤습니다. 단일 IDE만 사용하는 엔지니어 중 현재 2,400명이 Cursor를 사용하고 있으며, IntelliJ 사용자는 120명에 불과합니다.

image3.png
그림 2: IDE 사용량, 2025년 4월 ~ 2026년 7월. 검은색 선은 Scala 및 Java 탐색을 Metals 제공 소스패스로 이동한 최초의 내부 Metals v2 릴리스를 나타냅니다(아래 심층 분석 참조).

Scala 및 Java 파일 열기 이벤트

전체 IDE 사용량보다 더 밀접한 신호는 Metals v2가 크리티컬 패스에 있는 언어인 Scala 및 Java에 대해 각 IDE에서 열린 파일의 비율입니다. 2025년 10월 Cursor에 첫 번째 주요 Metals v2 개선 사항이 적용된 이후, Scala 및 Java 파일 열기 이벤트에서 Cursor의 점유율은 40%에서 78%로 상승했습니다. Scala와 Java는 IntelliJ가 가장 견고하게 자리 잡고 있던 영역이기 때문에 전체 IDE 도입 속도보다는 상승세가 느리지만, Cursor의 점유율은 매월 계속해서 상승하는 추세입니다.

image1.png
그림 3: IDE별 Scala 및 Java 파일 열기 이벤트, 2025년 4월 ~ 2026년 7월.

초기 외부 도입

Metals v2는 Databricks 포크로 시작되었지만, 목표는 항상 이 작업을 오픈 소스 커뮤니티로 다시 돌려보내는 것이었습니다. Cursor 팀 및 VirtusLab의 핵심 Metals 메인테이너들과 함께, 저희는 현재 안정적인 v1 릴리스를 대체할 안정적인 Metals v2 릴리스를 준비하고 있습니다.

저희는 Bazel 지원, 디버깅 및 테스트 개선에 초점을 맞추어 수백만 줄에 달하는 Java 코드베이스에서 Cursor가 원활하게 작동하도록 Metals v2에 기여하고 있습니다.—Kevin Niparko, Cursor
AI는 개발자가 IDE를 사용하는 방식을 변화시키고 있으며, Metals v2는 빠른 시작, 신뢰할 수 있는 코드베이스 오리엔테이션, 대규모 코드베이스를 위해 구축된 아키텍처 등 올바른 방향으로 나아가고 있습니다. Databricks는 이례적인 규모에서 이 접근 방식을 검증했으며, VirtusLab은 이 작업을 더 넓은 Scala 및 JVM 커뮤니티에 제공하는 데 기여하게 되어 기쁩니다.—Krzysztof Romanowski, Head of Development Productivity, VirtusLab

다른 대규모 JVM 코드베이스 담당자들과의 대화를 통해 Databricks에서 보았던 것과 동일한 패턴을 확인할 수 있었습니다. 강력한 SSH 원격 지원을 갖춘 Cursor, VS Code, Neovim에 대한 수요, 통합 툴링을 향한 플랫폼의 압박, 그리고 모노레포 규모에서 기존 JVM 언어 서버를 통해서는 확실한 해결책을 찾을 수 없었다는 점입니다.

저희는 한 달 전부터 Stripe에 Metals V2를 도입하기 시작했는데, Java 코드베이스에서 매우 잘 작동하는 모습, 엔지니어들의 열띤 반응, 그리고 메인테이너들과 함께 협업하고 배우는 즐거움에 끊임없이 감탄하고 있습니다.—Mahib Hosain, Developer Platform, Stripe

이것이 저희가 Metals v2에 바라는 모델입니다. 대규모 JVM 코드베이스를 위한 공유 인프라로서, 공개적으로 유지 관리되고, 대규모 환경에서 작동해야 하는 기업들에 의해 형성되는 것입니다. 지속적인 개발은 VirtusLab이 주도하고 있으니, 질문, 피드백 또는 기여는 GitHub 이슈 트래커 또는 metals@virtuslab.com을 통해 문의해 주세요.

심층 분석: Metals v2의 확장 방식

지금까지는 Metals v2의 도입 배경이었습니다. 나머지 내용은 2,600만 줄에 달하는 모노레포에서 지연 시간이 짧은 코드 인텔리전스를 구현한 엔지니어링 기술을 더 자세히 이해하고자 하는 독자들을 위한 것입니다.

에이전트가 대부분의 코드를 작성하기 때문에, 포괄적인 LSP(Language Server Protocol) 완성 및 리팩터링 범위보다 빠른 코드베이스 오리엔테이션이 더 중요했습니다. 이로 인해 문제가 좁혀졌지만 해결책이 명확해진 것은 아닙니다. 이처럼 거대한 Scala 및 Java Bazel 모노레포에 대해 설정하기 쉽고 지연 시간이 짧은 탐색 기능을 여전히 제공해야 했으며, 기존의 기성 LSP는 이러한 규모를 지원하도록 설계되지 않았기 때문입니다. 저희는 기본 원칙부터 시작하여 리포지토리 규모의 코드 인텔리전스를 구축해야 했고, 시작 시의 유용성을 측정하고 개선할 수 있는 핵심 지표로 삼아야 했습니다.

초기 인텔리전스 도달 시간(TTII) 정의

초기 인텔리전스 도달 시간(TTII)은 리포지토리를 연 후 에디터가 얼마나 빨리 유용해지는지를 측정합니다. 사용자 개입이 없다고 가정할 때, 언어 서버가 활성화되는 시점부터 가장 중요한 기능들을 사용할 수 있게 되는 시점까지의 시간을 측정합니다. 이러한 중요한 기능에는 워크스페이스 심볼의 퍼지 검색, 정의로 이동(jump-to-definition), 전체 리포지토리에서의 심볼 사용처 찾기 등이 포함됩니다.

Metals v2의 세 가지 레이어

Metals v2는 Scala 및 Java용 언어 서버입니다. 공식 Scala 언어 서버인 Metals v1에서 시작했지만, 목표 TTII에 도달하기 위해서는 단순히 미세 조정하는 것 이상의 작업이 필요했습니다. 저희는 이를 포크하여 리포지토리 인덱스, Scala 및 Java 컴파일러 파이프라인, 빌드 통합 경계라는 세 가지 핵심 레이어를 재작업했습니다.

  1. 빌드 프리(Build-free) 리포지토리 인덱스: Metals v2는 아래 섹션에서 설명하는 자체 mbt 인덱스를 사용하여 워크스페이스 소스를 직접 인덱싱함으로써 시작 크리티컬 패스에서 BSP(Build Server Protocol)를 제거합니다. Metals v1과의 핵심적인 차이점은 Metals가 이제 빌드 서버가 프로젝트 모델을 제공할 때까지 기다리는 대신 초기 프로젝트 모델을 직접 소유한다는 것입니다.
  2. Scala 및 Java를 위한 컴파일러 지원 대화형 파이프라인: Metals v2는 심볼 로딩을 빌드 제공 클래스패스(classpath)에서 Metals 제공 소스패스(sourcepath)로 이동하여, 진단 및 탐색이 마지막으로 성공한 컴파일의 오래된 스냅샷 대신 디스크의 실제 코드를 더 정확하게 반영할 수 있도록 합니다. 이를 위해 v1 코드베이스의 수많은 핵심 가정을 재고해야 했습니다.
  3. 메타데이터 우선 빌드 통합: Metals v2는 여전히 BSP를 사용하지만, 더 좁은 범위의 계약을 적용합니다. Metals v1은 진단을 위해 에디터 핫 패스에서 빌드 서버를 사용한 반면, Metals v2는 일상적인 진단을 빌드 서버 외부로 이동하고 BSP를 주로 종속성, 생성된 소스, 테스트 검색, 디버그 실행기 등의 빌드 메타데이터를 쿼리하는 데 사용합니다. 이러한 전환은 Metals v2용 BSP 서버 구현 장벽을 낮추며, 저희 내부의 Bazel BSP 서버는 이 모델이 대규모 Bazel 모노레포로 확장 가능하다는 것을 검증합니다.

아래 섹션에서는 각 레이어를 차례대로 살펴봅니다.

mbt 인덱스: 빌드 동기화 전 리포지토리 전반의 인텔리전스

mbt는 Metals Build Tool의 약자이며, mbt 인덱스는 TTII 또는 "즉시 유용함" 계약을 가능하게 하는 핵심 요소입니다. 이는 워크스페이스 소스의 콘텐츠 주소 지정 인덱스입니다. Metals는 git ls-files --stage 명령어를 사용하여 파일을 검색하고 Git 블롭 OID를 통해 재사용할 수 있는 인덱스 항목을 결정합니다. 빌드 동기화 전에 리포지토리 전반의 정보를 사용할 수 있으므로 Metals는 다음과 같은 "첫 단계" 질문에 답할 수 있습니다.

  • 교차 파일 참조에 대한 진단
  • 퍼지 심볼 검색
  • 외부 종속성이나 생성된 코드를 제외한 리포지토리 전반의 정의로 이동(jump-to-definition)
  • 문서별 bloom filter의 창의적인 활용을 통한 광범위한 find-references 및 find-implementations

mbt 인덱스는 사실상 소스 파일에서 파일 로컬 요약으로 매핑되는 해시 맵입니다. 각 엔트리는 파일의 패키지 선언, 소스 위치가 포함된 정의, 그리고 파일에서 참조되는 식별자를 위한 컴팩트한 bloom filter를 기록합니다. 정의는 워크스페이스 심볼 검색과 jump-to-definition 기능을 지원하며, bloom filter는 Metals가 더 정밀한 검사를 수행하기 전에 참조를 포함할 수 없는 파일을 빠르게 제외할 수 있도록 도와줍니다. 각 엔트리는 단 하나의 파일에서만 파생되므로 증분 업데이트가 매우 간단합니다. 파일이 변경되면 Metals는 해당 파일의 엔트리를 다시 계산하고 교체합니다.

저희 모노레포에서 압축되지 않은 지속성 mbt 인덱스의 크기는 936MB이며, 14만 2천 개 이상의 Scala, Java 및 Protobuf 파일에 걸쳐 약 290만 개의 심볼 정보를 포함하고 있습니다. 32개 코어 전체 CPU를 완전히 사용하는 클린 벤치마크 빌드에는 22초가 소요되며, 디스크에서 미리 빌드된 인덱스를 파싱하는 데는 5초가 걸립니다. 프로덕션 환경에서 저희는 서버를 시작하고, 오래된 mbt 인덱스를 로드하고, 이를 최신 git ls-files --stage 상태로 업데이트하고, Scala 및 Java 프레젠테이션 컴파일러를 재시작하는 데 걸리는 시간을 TTII로 측정합니다: p50 8.7초, p90 36.7초. 290만 개의 워크스페이스 심볼에 대한 유사(fuzzy) 심볼 검색 속도는 p50 10ms, p90 95ms입니다. TTII를 더 단축할 여지는 있지만, 현재 수치상으로는 당장 해결해야 할 병목 구간은 아닙니다.

Scala 파이프라인: 단일 컴파일러 인스턴스에서 2,400만 줄의 코드 처리하기

Scala 파이프라인은 컴파일 실행 전반에 걸쳐 심볼 테이블 정보를 캐싱하고 재사용하는 Scala 타입 검사기(type-checker)의 모드인 프레젠테이션 컴파일러(presentation compiler)를 중심으로 구축되었습니다. 이러한 재사용은 컴파일러의 lazy 심볼 해상도(lazy symbol resolution)와 결합되어, 단일 인스턴스가 전체 2,400만 줄의 Scala 코드베이스를 스코프 내에 유지하면서 p50 0.9초, p90 8.9초의 속도로 진단 결과를 게시할 수 있도록 합니다.

이 단일 Scala 컴파일러 인스턴스는 편집 중인 파일에 대해 Metals가 빌드 서버로부터 얼마나 많은 정보를 가지고 있는지에 따라 두 가지 모드 중 하나로 실행됩니다. 빌드 동기화(build sync) 전에는 fallback 컴파일러가 허용적인 관점을 취하여 리포지토리의 모든 소스 파일을 유효한 종속성 후보로 취급합니다. 덕분에 Bazel에서 아직 컴파일되지 않는 코드에서도 즉시 유용한 탐색이 가능해집니다. 빌드 동기화 후에는 precise 컴파일러가 빌드 서버가 보고하는 classpath 및 sourcepath 경계로 스스로를 제한하며, 이를 통해 진단 및 종속성 정보의 빌드 정확성을 보장합니다. precise 모드와 fallback 모드 모두 이처럼 거대한 sourcepath를 다루기 쉬운 상태로 유지하기 위해 동일한 두 가지 기술에 의존합니다:

  • 열리지 않은 소스를 위한 아웃라인 모드(Outline mode). 에디터에 열려 있지 않은 소스는 타입 검사 전에 메서드 본문이 제거됩니다. 이를 통해 대부분의 타입 검사 시간이 소요되는 메서드 본문에서의 작업을 피하면서 컴파일러에 필요한 타입 시그니처를 보존합니다.
  • 인메모리 소스 레이아웃 인덱스. Scala 소스 경로는 패키지 구조를 안정적으로 인코딩하지 않으므로, Metals는 파서를 실행하여 클래스를 소스 위치에 매핑하는 가벼운 인덱스를 구축한 다음, 파일 시스템을 스캔하는 대신 해당 인덱스를 통해 심볼을 로드합니다. fallback 컴파일러는 소스에서 다시 파싱하는 대신 Metals가 이미 가지고 있는 리포지토리 전체 데이터를 재사용하여 50ms 미만 만에 mbt 인덱스로부터 이 인덱스를 직접 구축합니다.

precise 모드는 세 번째 기술을 추가합니다. 즉, 전이적 종속성(transitive dependency) 소스를 sourcepath에 유지하므로, 서로 다른 Bazel 타겟의 파일 간 편집 내용이 Bazel이 새로운 classpath를 생성할 때까지 기다릴 필요 없이 에디터에 즉시 반영됩니다. 이는 Metals v1의 발목을 잡았던 제약 사항이었습니다.

단일 프레젠테이션 컴파일러 인스턴스에 2,400만 줄의 코드베이스를 유지하는 것은 원래 Scala 컴파일러가 설계된 목적을 크게 벗어나는 일입니다. Metals v2는 sourcepath를 확장하기 위해 공유된 기술 세트를 기반으로 구축된 두 가지 컴파일러 모드를 통해 이를 달성합니다. 에디터에서 가장 많이 사용되는 기능인 Jump-to-definition은 리포지토리 전체에서 p50 7ms, p90 575ms의 속도로 실행됩니다. Scala는 저희 엔지니어 대부분이 사용하는 언어이므로, 이 파이프라인은 Cursor가 저희 모노레포에서 IntelliJ의 진정한 대안이 되기 위해 반드시 작동해야만 했던 핵심 요소였습니다.

Java 파이프라인: Turbine으로 초당 100만 줄의 Java 코드 처리하기

Metals v2는 Java LSP 서피스를 javac API에 직접 구현합니다. 저희는 기존 Java 언어 서버(JDT 및 NetBeans 기반 구현체)의 재사용을 검토했으나, 두 가지 모두 Metals v1과 동일하게 빌드 중심적이었습니다. 이는 v2에서 제거하고자 했던 바로 그 결합이었습니다. 대신 javac를 기반으로 구축함으로써 Java가 저희 모노레포의 대부분을 차지하는 Scala와 Metals v2의 sourcepath 및 빌드 동기화 모델을 공유할 수 있게 되었으며, 필요한 Java 서피스는 저희가 직접 유지 관리할 수 있을 만큼 충분히 작았습니다.

javac API는 진단, 탐색, 시맨틱 하이라이팅(semantic highlighting) 및 기타 주요 LSP 메서드를 구현하기에 충분한 컴파일러 액세스를 제공했으며, 대화형 에디터 워크플로에서 중요한 부분적으로 깨진 코드에서도 잘 작동했습니다. Scala와 마찬가지로 리팩터링 및 자동 완성 같은 활성 편집 기능의 범위는 의도적으로 좁게 유지했습니다.

이 아키텍처의 주요 확장성 문제는 심볼 아웃라인 레이어에서 수백만 줄의 코드를 전이적으로 임포트하여 javac "enter" 단계에서 비정상적인 성능 저하를 유발하는 파일에서 나타났습니다. Metals v2는 이름 해상도(naming resolution) 오류의 유연한 처리를 포함하여 IDE 환경에서 작동하는 리포지토리 classpath를 생성하기 위해 수정된 버전의 Turbine 헤더 컴파일러를 사용하는 javaSymbolLoader: "turbine-classpath" 모드(기본 활성화됨)를 통해 이 문제를 해결합니다. Turbine은 단일 스레드에서 초당 약 100만 줄의 Java 코드를 처리하므로, Metals는 정기적인 주기로 전체 Java 코드베이스를 재컴파일할 수 있습니다. 이를 통해 거의 모든 파일 간 심볼을 sourcepath 대신 classpath에 유지하므로, javac "analyze" 단계가 대화형 사용을 위한 실질적인 한계인 초당 거의 10만 줄의 코드(저희 벤치마크 기준)에 가깝게 실행될 수 있습니다.

빌드 통합: 에디터 대기 시간 수준에서 28만 5천 개의 Bazel 타겟 쿼리하기

“빌드 동기화 전에 유용하다”는 것이 “빌드를 무시한다”는 의미는 아닙니다. Metals는 서드파티 종속성 또는 생성된 소스로의 탐색, 사용자 정의 셰이딩(shading) 규칙 준수, 테스트 스위트 검색 및 디버그 실행기 자동 구성 등을 포함한 다양한 롱테일 기능을 위해 여전히 빌드 그래프 충실도(build-graph fidelity)가 필요합니다. Databricks에서는 28만 5천 개의 Bazel JVM 타겟의 메타데이터를 에디터 대기 시간 수준에서 쿼리할 수 있도록 만드는 것을 의미합니다.

이 레이어의 프로덕션 구현체는 Go로 작성되고 저희 Bazel 규칙에 맞게 조정된 내부 BSP 서버입니다. 이 BSP 서버는 이번 오픈 소스 릴리스에 포함되어 있지 않지만, 그 설계 선택은 다른 Bazel BSP 구현체로 이식할 만한 가치가 있습니다.

첫째, Metals v2는 사용자가 명시적으로 요청하지 않는 한 BSP 서버를 통해 Bazel을 호출하지 않습니다. 대규모 모노레포에서는 백그라운드 IDE 동기화가 Bazel 잠금(lock)을 획득하여 개발자가 시작한 빌드와 경쟁할 수 있으므로, 빌드 동기화는 시작 시 동작이나 백그라운드 유지 관리 작업이 아니라 사용자의 명시적인 작업으로 수행됩니다. 결과 메타데이터는 반복되는 레이블, 경로 및 리포지토리 접두사에 대한 상수 풀링(constant pooling)을 통해 확장되는 JSON 스냅샷에 저장됩니다. 사용자가 동기화하면 BSP 서버는 이 스냅샷에 타겟을 점진적으로 추가하고 BSP를 통해 업데이트된 메타데이터를 제공합니다.

둘째, 공유되는 동기화 구성 형식이 없습니다. 사용자는 탐색 또는 진단 정확도를 향상시키기 위해 편집 중에 필요에 따라 개별 파일 또는 디렉터리를 동기화합니다. 저희 경험상, 미리 정의된 동기화 세트는 시간이 지남에 따라 커지고 팀 간에 복사되며, 개발자에게 실제로 필요한 집중 동기화보다 느려집니다.

세 가지 레이어의 결합

빌드가 필요 없는 인덱스, 컴파일러 기반 파이프라인, 그리고 메타데이터 우선 빌드 통합이 결합되어 수백만 줄에 달하는 Bazel 코드베이스에서 대기 시간이 짧은 코드 인텔리전스를 제공합니다. 이를 Apache 2.0 라이선스로 오픈 소스화함으로써, 기존 JVM 언어 서버가 확실한 대안을 제시하지 못했던 규모에서 코딩 에이전트 및 가벼운 에디터를 풍부한 Scala 및 Java 탐색 기능과 결합할 수 있는, 이전에는 존재하지 않았던 선택지를 더 넓은 생태계에 제공하고자 합니다.

(이 글은 AI의 도움을 받아 번역되었습니다. 원문이 궁금하시다면 여기를 클릭해 주세요)

최신 게시물을 이메일로 받아보세요

블로그를 구독하고 최신 게시물을 이메일로 받아보세요.