TECH 으로 돌아가기
TECH HACKER NEWS 오늘 8분 읽기 29 READS

러스트 LSP는 왜 만들기 어려운가: rust-analyzer와 Rust Glancer가 마주한 벽

러스트 LSP는 왜 만들기 어려운가: rust-analyzer와 Rust Glancer가 마주한 벽
SOURCE IMAGE · HACKER NEWS

코드 편집기에서 자동완성, 정의로 이동, 호버 문서 같은 기능을 제공하는 언어 서버(LSP)는 이제 개발 환경의 기본값이 됐다. 하지만 그 뒤편에서 벌어지는 일은 겉보기만큼 단순하지 않다. 실험적 러스트 언어 서버인 Rust Glancer를 개발 중인 저자는 rust-analyzer 코드를 오래 들여다본 경험을 바탕으로, LSP 개발이 결국 "불완전한 정보에서 유용한 답을 만들어내는 일"임을 설명한다. rust-analyzer 팀의 대표적 저자였던 matklad의 마지막 블로그 글이 2023년에 머물러 있는 지금, 이 글은 러스트 도구 내부 구조를 다시 짚어보는 드문 기록이다.

컴파일러와 다른 '완료'의 정의

LSP 서버는 두 개의 상반된 얼굴을 가진다. 하나는 프로토콜을 구현해 요청을 받고 응답을 돌려주는 서버이고, 다른 하나는 그 질의가 실제로 의미 있는 결과를 내도록 색인된 상태를 관리하는 부분이다. tower-lsp-server 같은 라이브러리가 있으니 앞쪽은 해결됐다고 여기기 쉽지만 실상은 그렇지 않다. 문제는 클라이언트가 보내는 initialize 요청에 언제 응답할 것인가에서부터 시작된다. 서버는 이 시점에야 비로소 어떤 코드베이스를 다루는지 알게 되는데, 질의에 답하려면 색인이 필요하고 색인에는 수십 초가 걸릴 수 있다. 이를 다 기다렸다가 응답하면 편집기가 한동안 먹통이 된다.

여기서 컴파일러와 LSP의 근본적 차이가 드러난다. 컴파일러의 완료는 이진적이다. 716개 크레이트 중 715개만 컴파일된 상태는 사실상 실패다. 반면 LSP는 첫 밀리초에 완전한 정보가 필요하지 않다. 최대한 빨리 무언가 쓸 만한 결과를 주면 된다. 그래서 두 서버 모두 설정만 검증하고 곧바로 응답한다. 차이라면 rust-analyzer는 응답 직후 워크스페이스 탐색을 예약하는 반면, Rust Glancer는 첫 질의가 들어올 때까지 수동적으로 대기한다는 점이다.

파일시스템이라는 유령

핸드셰이크가 끝나면 didOpen, inlayHint, documentSymbol 같은 실제 질의가 쏟아진다. LSP 프로토콜은 개발자에게 파일시스템을 잊으라고 말한다. 저장되지 않은 문서가 흔하니 문서와 편집 이벤트만 다루라는 것이다. 그러나 단일 파일만 고립해서 분석하는 것은 곧 한계에 부딪힌다. 결국 서버는 스스로 파일시스템에 접근하되 LSP가 소스의 진실이라는 전제와 동기화해야 한다. 편집이 편집기 밖에서 일어날 수도 있고 클라이언트가 이를 성실히 알려주지 않을 수도 있다. 그래서 소스를 메모리에 올려 가상 파일시스템(VFS)으로 선언하고, 변경마다 '소스 세대(source generation)' 식별자를 갱신해 내부 상태의 일관성을 지킨다.

동시에 실행 중인 질의도 문제다. 참조 검색은 무겁고 호버는 가볍다. 읽기 질의를 하나씩 처리할 수 없으니 병렬로 돌려야 하는데, 상태가 바뀌면 진행 중이던 작업은 낡은 상태에 대한 헛수고가 된다. 따라서 변경 질의와 읽기 질의를 분리하고, 상태가 바뀌면 진행 중 작업을 취소하는 루프가 필요하다. 비동기를 쓴다면 들어오는 메시지를 직렬화해 didOpen과 didChange가 뒤바뀌지 않도록 해야 한다. 다행히 LSP는 workspace/inlayHint/refresh 같은 도구를 제공해 "지금 다시 물어봐 달라"고 요청할 수 있다. 그러나 문서 심볼처럼 갱신 요청이 불가능한 질의도 있어, 처음부터 보내지 않으면 오래된 채로 남는다.

계층을 쌓아 올라가는 분석

질의마다 요구하는 분석 깊이가 다르다는 점이 핵심이다. 문서 심볼은 파일 하나만 파싱하면 구조체, 트레이트, 함수 같은 항목을 곧바로 알 수 있어 요청 시점에 즉석 처리도 가능하다. 반면 fn foo(a: Bar)의 Bar 위 호버는 의미 분석이 필요하다. 파싱으로 아이템 트리를 만들고, 모듈을 해석해 정의 맵(무엇이 어디서 보이는가)을 세운 뒤, 커서 아래 대상을 찾아 문서를 추출해야 한다. 인레이 힌트는 더 까다롭다. 함수 본문 내부에서 나타나며 최소한 타입 추론을 요구하기 때문이다.

최종 보스는 textDocument/references다. 함수 정의 위에서 참조 찾기를 누르면 서버는 워크스페이스 전체에서 그 함수의 모든 사용처를 찾아야 한다. Option처럼 흔한 이름이라면 수천 개의 본문을 뒤지며 바로 그 Option을 다른 동명 항목과 구별해야 한다. 모든 본문을 선형으로 훑을 수는 없으니, 텍스트 매칭으로 후보 파일을 추린 '참조 검색 계획'을 세운다. 저자는 LSP 개발이 이런 병리적 경우(pathological case)와의 싸움이며, 그것이 바로 어려움의 본질이라고 강조한다.

색인하지 않는 색인, salsa

두 서버는 파싱·아이템 트리·정의 맵·의미 계층·본문 계층이라는 유사한 구조를 갖지만 계산 방식이 다르다. rust-analyzer는 증분 데이터베이스 salsa를 쓴다. 입력과 변환 로직을 정의하면 출력이 지연 계산되고 메모이즈되며, 입력이 바뀌면 관련 부분만 무효화·재계산된다. 별도의 색인 과정 없이 필요할 때 상태를 물으면 salsa가 계산해 주는 구조다. 다만 이 방식만으로는 초기 편집이 버벅일 수 있어, rust-analyzer는 기본적으로 캐시 프라이밍을 켜 의미 계층까지의 정보를 워크스페이스 전반에 미리 계산해 둔다. 본문 분석은 그 뒤로 미룰 수 있다.

실무자 관점에서 이 글이 주는 함의는 분명하다. 러스트 도구의 반응성은 우연이 아니라 '언제 무엇을 계산하지 않을지'에 대한 설계 선택의 결과다. 다만 이 글은 저자 스스로 밝히듯 정밀한 설계 문서가 아니라 내부 구조에 대한 흥미를 유도하는 개괄이며, 타입 추론이나 본문 분석 같은 핵심 난제는 의도적으로 뒤로 미뤄 둔 상태다. 그럼에도 자신의 편집기가 왜 어떤 순간에는 즉각 답하고 어떤 순간에는 잠시 멈추는지를 이해하려는 개발자에게, 그 지연의 이면에 놓인 설계의 무게를 가늠하게 해준다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://rust-glancer.github.io/blog/why-lsp-is-hard/
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기
처리 중...