<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Rename on monkshark.dev</title><link>https://monkshark.github.io/tags/rename/</link><description>Recent content in Rename on monkshark.dev</description><generator>Hugo -- gohugo.io</generator><language>ko</language><lastBuildDate>Thu, 14 May 2026 09:30:00 +0900</lastBuildDate><atom:link href="https://monkshark.github.io/tags/rename/index.xml" rel="self" type="application/rss+xml"/><item><title>#8 - top-level object 이름을 바꾸려다 fork 까지 간 경위</title><link>https://monkshark.github.io/p/page-ide-kls-fork-top-level-object-rename/</link><pubDate>Thu, 14 May 2026 09:30:00 +0900</pubDate><guid>https://monkshark.github.io/p/page-ide-kls-fork-top-level-object-rename/</guid><description>&lt;p&gt;심볼 위에 캐럿을 두고 &lt;code&gt;Shift+F6&lt;/code&gt; 을 누르면 이름을 바꾸는 다이얼로그가 뜬다. 그건 LSP 의 &lt;code&gt;textDocument/rename&lt;/code&gt; 가 받아서 워크스페이스 전체에 걸친 &lt;code&gt;WorkspaceEdit&lt;/code&gt; 으로 돌려준다.&lt;/p&gt;
&lt;p&gt;PAGE 에 붙인 kotlin-language-server 는 거의 다 잘 해 줬다. &lt;code&gt;class&lt;/code&gt; 도, 함수도, 프로퍼티도, 로컬 변수도. 단 하나가 안 됐다 — top-level &lt;code&gt;object&lt;/code&gt; 가 죽었다.&lt;/p&gt;
&lt;p&gt;이 글은 KLS 의 한 분기를 한 클래스만큼 넓히기 위해 fork 를 떠 와 PAGE 에 번들하기까지의 회고다. 코드 변경은 한 줄이었다. 그 한 줄을 PAGE 의 빌드에 꽂는 과정이 한 줄보다 훨씬 길었다.&lt;/p&gt;
&lt;h2 id="죽는-자리"&gt;&lt;a href="#%ec%a3%bd%eb%8a%94-%ec%9e%90%eb%a6%ac" class="header-anchor"&gt;&lt;/a&gt;죽는 자리
&lt;/h2&gt;&lt;p&gt;테스트 샘플의 &lt;code&gt;object Hello&lt;/code&gt; 위에서 &lt;code&gt;Shift+F6&lt;/code&gt; 을 누르면 PAGE 의 콘솔에 KLS 서버가 던진 스택 트레이스가 줄줄이 흘렀다. 핵심 한 줄.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kotlin.NotImplementedError: TopLevelDescriptorProvider not found
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; at org.jetbrains.kotlin.resolve.lazy.NoTopLevelDescriptorProvider...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; at ...LazyTopDownAnalyzer.analyzeDeclarations...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; at org.javacs.kt.CompiledFile.contentAndOffsetFromElement...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;CompiledFile.contentAndOffsetFromElement&lt;/code&gt; 가 호출자다. KLS 가 &lt;code&gt;prepareRename&lt;/code&gt; / &lt;code&gt;rename&lt;/code&gt; 을 처리할 때 선언 식별자 토큰 자리에 가짜 reference 표현식 을 끼워 분석기를 한 번 더 돌리는 단계인데, 거기서 &lt;code&gt;LazyTopDownAnalyzer&lt;/code&gt; 가 top-level 컨텍스트의 descriptor provider 를 못 찾고 죽는 거였다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;class&lt;/code&gt; 일 때는 안 죽는다. 같은 자리에 &lt;code&gt;class Hello&lt;/code&gt; 가 있으면 rename 이 정상 동작했다. 그래서 시야가 한쪽으로 좁혀졌다.&lt;/p&gt;
&lt;h2 id="kls-가-어떻게-처리하는가"&gt;&lt;a href="#kls-%ea%b0%80-%ec%96%b4%eb%96%bb%ea%b2%8c-%ec%b2%98%eb%a6%ac%ed%95%98%eb%8a%94%ea%b0%80" class="header-anchor"&gt;&lt;/a&gt;KLS 가 어떻게 처리하는가
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;CompiledFile.contentAndOffsetFromElement&lt;/code&gt; 의 로직은 (단순화하면) 이렇다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;사용자가 선언 식별자 자리에서 어떤 액션을 호출했다 (rename, definition jump 등)&lt;/li&gt;
&lt;li&gt;그 위치에 그대로 분석을 돌리면 컴파일러가 선언 컨텍스트 를 처리하려 들면서 top-level 처리에 의존한다&lt;/li&gt;
&lt;li&gt;분석기는 이미 우리 손에 있는 파일 단위 컨텍스트만 가지고 있으니 top-level provider 가 없어 예외&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이걸 우회하는 트릭이 들어가 있다. 식별자 자리를 가짜 &lt;code&gt;val x: &amp;lt;Name&amp;gt;&lt;/code&gt; 으로 감싸 reference expression 자리에 놓고 분석을 돌린다. 분석기 입장에서는 새 변수의 타입 reference 가 들어왔다고 보고 top-level 의존을 거치지 않는다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;KtClass&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;psi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;elementType&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;KtTokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IDENTIFIER&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;val x: &amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;surroundingContent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;psi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textRange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startOffset&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// ... 다른 종류의 선언들
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;KtClass&lt;/code&gt; 만 본다. &lt;code&gt;object&lt;/code&gt; 는 &lt;code&gt;KtClass&lt;/code&gt; 가 아니라 &lt;code&gt;KtObjectDeclaration&lt;/code&gt; 인데, 둘은 공통 슈퍼타입 &lt;code&gt;KtClassOrObject&lt;/code&gt; 를 공유한다. 분기가 &lt;code&gt;KtClass&lt;/code&gt; 로 좁혀 있으면 object 는 이 트릭을 받지 못한 채 원본 위치 그대로 분석에 던져진다 → top-level descriptor provider 가 호출된다 → 죽는다.&lt;/p&gt;
&lt;h2 id="우회-시도"&gt;&lt;a href="#%ec%9a%b0%ed%9a%8c-%ec%8b%9c%eb%8f%84" class="header-anchor"&gt;&lt;/a&gt;우회 시도
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;KtClassOrObject&lt;/code&gt; 한 글자 차이로 fork 가 정답인 게 보이는 자리다. 그래도 클라이언트(PAGE) 측에서 우회할 수 있을지를 먼저 본다 — fork 는 마지막 수단이다.&lt;/p&gt;
&lt;p&gt;후보 1: 클라이언트가 object 의 IDENTIFIER 토큰 자리를 KLS 로 보내지 않고, 같은 이름을 가진 다른 reference 자리 (사용처) 로 옮긴 다음 거기서 rename 을 요청한다. KLS 가 reference 자리에서는 안 죽으니 동작은 할 수 있다. 다만 사용처가 없는 object 는 처리 못 한다. 그리고 IDE 의 동작이 &amp;ldquo;선언 위에서 누른 rename 이 사용처 위에서 누른 rename 과 다르게 처리된다&amp;rdquo; 는 미묘한 상태가 된다. 사용자가 코드에서 같은 것 을 다르게 다루도록 강요받는다.&lt;/p&gt;
&lt;p&gt;후보 2: 클라이언트가 임시로 &lt;code&gt;object&lt;/code&gt; 키워드를 &lt;code&gt;class&lt;/code&gt; 로 바꾼 텍스트를 KLS 에 보내고, 응답으로 받은 edit 을 적용하기 전에 원본 텍스트로 되돌린다. 의미가 다른 키워드를 가짜로 바꿔 보내는 거라 KLS 가 응답에 포함시킬 다른 검증 (예: 동반 객체 사용 위치, expect/actual 매칭) 이 어긋날 위험이 있다. 그리고 어차피 KLS 의 한 줄이 너무 좁다 는 사실 자체는 안 사라진다.&lt;/p&gt;
&lt;p&gt;후보 3: KLS 의 &lt;code&gt;rename&lt;/code&gt; 자체를 끄고 클라이언트가 텍스트 grep + 컴파일러 없는 휴리스틱으로 rename 을 한다. 가장 자유롭지만 가장 위험하다. 같은 이름의 다른 스코프, import alias, 패키지 동명 클래스 같은 게 줄줄이 깨진다. (이 길은 나중에 references 에서 결국 한 번 가게 되지만, 그건 별개 글이다.)&lt;/p&gt;
&lt;p&gt;세 후보 모두 KLS 의 한 줄 분기를 우회하기 위해 클라이언트가 KLS 보다 더 많은 일 을 떠안는다. 한 클래스만큼만 분기를 넓히면 되는 자리에 클라이언트가 의미론을 떠안는 건 비대칭이다. fork 가 더 정직했다.&lt;/p&gt;
&lt;h2 id="패치"&gt;&lt;a href="#%ed%8c%a8%ec%b9%98" class="header-anchor"&gt;&lt;/a&gt;패치
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;Monkshark/kotlin-language-server&lt;/code&gt; 로 fork 떠서 &lt;code&gt;1.3.13-page-1&lt;/code&gt; 태그를 박았다. 변경은 세 가지.&lt;/p&gt;
&lt;p&gt;첫째, &lt;code&gt;KtClass&lt;/code&gt; 분기를 &lt;code&gt;KtClassOrObject&lt;/code&gt; 로 넓힌다. 한 글자 차이.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;KtClassOrObject&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;psi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;elementType&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;KtTokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IDENTIFIER&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// Converting class/object name identifier: Use a fake property with the class/object name as type
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// (PAGE patch: also covers KtObjectDeclaration, not just KtClass)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;val x: &amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;surroundingContent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;psi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textRange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startOffset&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;둘째, 서버 시작 로그에 패치 식별자를 단다. PAGE 가 올바른 fork 를 잡고 있는지 콘솔에서 한눈에 확인하려는 용도.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nc"&gt;LOG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Kotlin Language Server: Version &lt;/span&gt;&lt;span class="si"&gt;${VERSION ?: &amp;#34;?&amp;#34;}&lt;/span&gt;&lt;span class="s2"&gt; (PAGE patch: KtClassOrObject reference)&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;셋째, KLS 의 &lt;code&gt;buildSrc/settings.gradle.kts&lt;/code&gt; 와 root &lt;code&gt;settings.gradle.kts&lt;/code&gt; 에 Foojay resolver plugin 을 붙인다. 이건 KLS 자체 빌드용 — JDK 21 환경에서 toolchain 이 자동으로 끌어와지도록 한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;plugins&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;org.gradle.toolchains.foojay-resolver-convention&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;0.9.0&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;세 변경 합쳐 17줄. 그중 의미 있는 한 줄은 &lt;code&gt;KtClass&lt;/code&gt; → &lt;code&gt;KtClassOrObject&lt;/code&gt; 다.&lt;/p&gt;
&lt;h2 id="page-에-꽂기"&gt;&lt;a href="#page-%ec%97%90-%ea%bd%82%ea%b8%b0" class="header-anchor"&gt;&lt;/a&gt;PAGE 에 꽂기
&lt;/h2&gt;&lt;p&gt;fork 가 GitHub Releases 에 &lt;code&gt;server.zip&lt;/code&gt; 으로 올라가 있다. PAGE 의 &lt;code&gt;:page:app&lt;/code&gt; 빌드가 그걸 받아 Compose resources 자리에 풀어 둔다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// page/app/build.gradle.kts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;klsVersion&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;1.3.13-page-1&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;klsDownloadUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;https://github.com/Monkshark/kotlin-language-server/releases/download/&lt;/span&gt;&lt;span class="si"&gt;$klsVersion&lt;/span&gt;&lt;span class="s2"&gt;/server.zip&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;klsResourcesDir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buildDirectory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;composeResources&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;klsServerDir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;klsResourcesDir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;common/lsp/server&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;다운로드 작업은 &lt;code&gt;DownloadKlsTask&lt;/code&gt; 라는 abstract task 로 떼 뒀다. 이건 fork 자체와는 별개의 모서리 — Gradle configuration cache 와 호환되려면 task action 안에서 project / build script 의 상태를 직접 참조하면 안 되고, &lt;code&gt;@get:Input&lt;/code&gt; / &lt;code&gt;@get:OutputFile&lt;/code&gt; 로 선언된 프로퍼티만 거쳐야 한다. 처음엔 람다 안에서 &lt;code&gt;project.layout&lt;/code&gt; 을 그대로 썼더니 cache miss 가 났고, 그 한 번의 통고 후로 task 를 abstract 로 정리했다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DownloadKlsTask&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DefaultTask&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nd"&gt;@get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;Input&lt;/span&gt; &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nd"&gt;@get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;Optional&lt;/span&gt; &lt;span class="nd"&gt;@get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;Input&lt;/span&gt; &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;localZip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nd"&gt;@get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;OutputFile&lt;/span&gt; &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RegularFileProperty&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nd"&gt;@TaskAction&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;download&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* curl + checksum + atomic rename */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;서버 시동 후 콘솔에 패치 식별자가 떴다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[lsp:log/Info] main Kotlin Language Server: Version 1.3.13 (PAGE patch: KtClassOrObject reference)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[lsp:log/Info] main Connected to client
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;object Hello&lt;/code&gt; 위에서 &lt;code&gt;Shift+F6&lt;/code&gt; 을 다시 눌렀다. 다이얼로그가 떴고, 새 이름을 입력하고 엔터를 누르자 &lt;code&gt;Greeter.kt&lt;/code&gt; 의 선언과 &lt;code&gt;Main.kt&lt;/code&gt; 의 두 호출이 한 번에 바뀌었다. 한 줄짜리 패치가 한 시나리오를 통째로 살렸다.&lt;/p&gt;
&lt;h2 id="돌아보면"&gt;&lt;a href="#%eb%8f%8c%ec%95%84%eb%b3%b4%eb%a9%b4" class="header-anchor"&gt;&lt;/a&gt;돌아보면
&lt;/h2&gt;&lt;p&gt;fork 는 한 줄 패치라고 해서 무게가 가벼운 일이 아니다. 떠서 끝이 아니라 들고 다녀야 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;업스트림 KLS 가 다음 릴리스를 내면 패치 한 줄을 rebase 해서 다시 빌드하고 태그를 박고 PAGE 의 &lt;code&gt;klsVersion&lt;/code&gt; 을 올려야 한다. 같은 자리가 또 만져졌다면 conflict 해결도 따라온다.&lt;/li&gt;
&lt;li&gt;PAGE 의 빌드는 fork 의 &lt;code&gt;server.zip&lt;/code&gt; 에 의존하니 fork repo 가 죽으면 PAGE 의 LSP 가 죽는다. PAGE 의 다른 의존성과는 비대칭이다.&lt;/li&gt;
&lt;li&gt;콘솔에 PAGE patch 식별자를 박은 건 작은 디테일 같았는데, 나중에 어떤 버그를 추적할 때 &amp;ldquo;이 KLS 가 fork 인가 업스트림인가&amp;rdquo; 를 한 번에 알 수 있는 가장 빠른 신호가 됐다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;언어 서버 한 줄을 위해 fork 를 떠도 되는가 라는 질문에 답을 한 줄로 하면 — 한 줄로 끝나는 일이 아니다 — 가 정답일 것이다. 그래도 이번엔 정답이 fork 였다고 본다. 클라이언트가 의미론을 떠안는 우회보다 KLS 의 분기를 한 클래스만큼 넓히는 쪽이 더 정직했고, 그 정직함이 다음 사람 (나일 수도 있고 다른 사람일 수도 있다) 이 KLS 의 동작을 추적할 때 헷갈리지 않게 해 준다.&lt;/p&gt;
&lt;p&gt;다음 자리는 KLS 가 못 고치는 자리 — 의미 기반 references — 다. 이번엔 fork 가 답이었지만, 그 일은 fork 도 답이 아니었다. 그건 다음 글이다.&lt;/p&gt;</description></item></channel></rss>