1. 개요
- Neovim에서 Spring Boot와 Maven 기반 Java 프로젝트를 개발하기 위한 설정을 기록한 내용
- 목표는 무거운 IDE의 모든 기능을 복제하는 것이 아니라 다음 작업을 Neovim 안에서 처리하는 것.
- Java 문법 강조와 들여쓰기
- 클래스, 메서드 정의와 참조 탐색
- 타입 및 문서 확인
- 자동완성, 이름 변경, Code Action
- import 정리와 코드 포맷
- Maven 또는 Gradle 빌드
- 프로젝트 전체, 현재 클래스, 현재 메서드 단위 테스트
1.1 읽기 전 알아둬야할 내용
<leader>는 Neovim의 leader 키다.
2. 이 설정들이 필요한 이유
- Vim은 그저 텍스트 편집기 자체에 불과함.
- Java를 개발하기에 순수한 Vim은 부족한 점이 있음.
- Vim에서 Java Project의 구조와 Type들에 대한 정보를 해석시키게 하는 설정이 필요.
- 단순 문자열 검색 외에 아래와 같은 기능들이 추가로 필요
- Classpath
- Maven Dependency 해석
- Type, Method 시그니처 이해 등
- 해당 설정을 위해선 아래와 같은 구성요소들이 필요하고, 그에 따른 역할들을 정리함
| 구성요소 |
역할 |
| JDK |
Java 컴파일과 실행에 필요한 기본 환경 |
| Maven 또는 Gradle |
의존성, 빌드, 테스트 실행 |
Eclipse JDT Language Server (jdtls) |
정의, 참조, 자동완성, 진단, 리팩터링 제공 |
| Neovim LSP |
Neovim과 jdtls 사이의 통신 담당 |
| Treesitter |
Java 구문을 구조적으로 분석하여 강조와 들여쓰기 제공 |
| Completion |
LSP가 제공한 완성 후보를 입력창에 표시 |
jdtls가 Java IDE 기능의 핵심
- Treesitter는 코드를 보기 좋게 만드는 역할에 불과함
- 타입이나 Maven 의존성을 이해하지는 않는다.
- Maven과 Gradle은 LSP와 별개다.
- LSP가 정상이어도 빌드 도구가 없으면 빌드와 테스트 단축키는 실행할 수 없다.
3. 외부 프로그램 준비(필자 기준, M2 Macbook Air)
- 현재 환경은 JDK와 Maven은 SDKMAN,
jdtls는 Homebrew로 관리한다.
sdk list java
sdk install java
sdk list maven
sdk install maven
brew install jdtls
- 프로젝트에
mvnw 또는 gradlew가 있으면 시스템 Maven/Gradle보다 wrapper를 우선 사용한다.
- wrapper에 실행 권한이 없다면 권한을 추가한다.
chmod +x mvnw
chmod +x gradlew
java -version
mvn -version
command -v jdtls
- 현재 기록 시점의 환경
- Java 21.0.2
- Maven 3.9.9
- jdtls 1.56.0
4. 설치하는 플러그인
| 플러그인 |
역할 |
Java 전용 여부 |
nvim-treesitter/nvim-treesitter |
Java 구문 강조와 들여쓰기 |
공통 |
neovim/nvim-lspconfig |
LSP 설정 기반과 서버별 기본값 제공 |
공통 |
hrsh7th/nvim-cmp |
자동완성 목록 UI |
공통 |
hrsh7th/cmp-nvim-lsp |
LSP 완성 기능을 nvim-cmp에 연결 |
공통 |
williamboman/mason.nvim |
LSP와 개발 도구 설치 관리 UI |
공통 |
williamboman/mason-lspconfig.nvim |
Mason과 LSP 설정 연결 |
공통 |
- 현재 Java 설정에는
nvim-jdtls, nvim-dap, neotest를 사용하지 않는다.
jdtls는 Mason이 아니라 Homebrew로 설치된 외부 실행 파일을 직접 사용한다.
- Mason과 mason-lspconfig는 Lua, Markdown 등 다른 언어와 공유하는 개발 기반이다. 현재 Java 연결 자체는 Neovim 0.11의
vim.lsp.config()로 직접 구성한다.
5. 플러그인 설정
5.1 nvim-treesitter
- Treesitter는 Java 코드를 구문 단위로 분석하여 문법 강조와 들여쓰기에 사용한다.
- 설치된 최신
main 계열에서는 예전 require("nvim-treesitter.configs").setup() 대신 parser 설치와 vim.treesitter.start()를 직접 설정한다.
{
"nvim-treesitter/nvim-treesitter",
lazy = false,
build = ":TSUpdate",
config = function()
local parsers = {
"java",
"xml",
"groovy",
"kotlin",
}
require("nvim-treesitter").install(parsers)
vim.api.nvim_create_autocmd("FileType", {
pattern = { "java", "xml", "groovy", "kotlin" },
callback = function()
if pcall(vim.treesitter.start) then
vim.bo.indentexpr = "v:lua.require'nvim-treesitter'.indentexpr()"
end
end,
})
end,
}
- parser를 수동으로 관리할 때는 다음 명령을 사용한다.
:TSInstall java
:TSUpdate
5.2 nvim-lspconfig
nvim-lspconfig는 여러 Language Server를 Neovim에 연결하기 위한 공통 기반이다.
- 현재 Java 설정은 Neovim 0.11의 새 API인
vim.lsp.config()로 jdtls를 등록한다.
- Java 파일을 열 때만 서버를 구성하며, 일반 텍스트나 Markdown 파일에서는 Java LSP를 실행하지 않는다.
{
"neovim/nvim-lspconfig",
dependencies = {
"hrsh7th/cmp-nvim-lsp",
},
}
5.3 nvim-cmp와 cmp-nvim-lsp
nvim-cmp는 자동완성 목록을 표시하고 선택한 후보를 입력한다.
cmp-nvim-lsp는 Neovim이 자동완성을 지원한다는 정보를 jdtls에 전달하고 LSP 후보를 completion source로 연결한다.
{
"hrsh7th/nvim-cmp",
dependencies = {
"hrsh7th/cmp-nvim-lsp",
},
config = function()
local cmp = require("cmp")
cmp.setup({
snippet = {
expand = function(_) end,
},
mapping = cmp.mapping.preset.insert({
["<C-Space>"] = cmp.mapping.complete(),
["<CR>"] = cmp.mapping.confirm({ select = true }),
}),
sources = {
{ name = "nvim_lsp" },
},
})
end,
}
- 완성 목록은 입력 중 자동으로 나타난다.
<C-Space>가 tmux의 <prefix>와 같은 경우 Neovim으로 해당 키를 보내려면 <prefix><prefix>를 사용한다.
- 대부분은 자동으로 열린 목록에서
<CR>로 확정하면 되므로 수동 호출을 자주 사용할 필요는 없다.
5.4 mason.nvim과 mason-lspconfig.nvim
- Mason은 외부 개발 도구를 Neovim 안에서 설치하고 상태를 확인하는 공통 관리 도구다.
:Mason으로 관리 화면을 연다.
- 현재
jdtls는 Mason 관리 대상이 아니라 Homebrew 실행 파일이므로 Mason 설정과 Java 실행 경로를 혼동하지 않는다.
{ "williamboman/mason.nvim", build = ":MasonUpdate", config = true },
{
"williamboman/mason-lspconfig.nvim",
dependencies = { "williamboman/mason.nvim" },
}
6. jdtls 설정
6.1 프로젝트 루트 탐지
- Java LSP는 단독 파일이 아니라 Maven, Gradle 또는 Git 프로젝트 안에서만 시작한다.
- 다음 파일 중 하나를 Java 파일의 상위 폴더에서 찾는다.
local root_markers = {
"mvnw",
"gradlew",
"settings.gradle",
"settings.gradle.kts",
"pom.xml",
"build.gradle",
"build.gradle.kts",
"build.xml",
".git",
}
- 일반적인 Spring Boot Maven 프로젝트라면
pom.xml 또는 mvnw가 있으므로 자동으로 활성화된다.
- 모노레포에서는 상위
.git과 하위 pom.xml 중 어느 경로를 root로 사용할지 별도 조정이 필요할 수 있다.
6.2 프로젝트별 jdtls workspace
jdtls는 프로젝트 인덱스와 상태를 저장할 workspace가 필요하다.
- 여러 프로젝트가 같은 workspace를 공유하면 인덱스가 충돌할 수 있으므로 프로젝트 경로를 해시하여 별도 폴더를 사용한다.
local function project_id(root)
local name = vim.fn.fnamemodify(root, ":p:h:t")
local hash = vim.fn.sha256(root):sub(1, 12)
return name .. "-" .. hash
end
local function workspace_dir(root)
return table.concat({
vim.fn.stdpath("cache"),
"jdtls",
"workspace",
project_id(root),
}, "/")
end
6.3 LSP 등록과 자동 활성화
java와 jdtls가 모두 실행 가능하고 프로젝트 root가 발견되었을 때만 연결한다.
- 단일 Java 파일 지원은 끈다.
vim.lsp.config("jdtls", {
cmd = { "jdtls" },
filetypes = { "java" },
single_file_support = false,
capabilities = require("cmp_nvim_lsp").default_capabilities(),
settings = {
java = {
configuration = {
updateBuildConfiguration = "interactive",
},
eclipse = {
downloadSources = true,
},
format = {
enabled = true,
},
import = {
gradle = {
enabled = true,
wrapper = { enabled = true },
},
maven = {
enabled = true,
},
},
maven = {
downloadSources = true,
},
signatureHelp = {
enabled = true,
},
},
},
})
vim.lsp.enable("jdtls")
- 실제 설정은
~/.config/nvim/lua/plugins/java_ide.lua에 있다.
- 실제 파일에서는 root 탐지, 프로젝트별 workspace, Maven/Gradle wrapper, 선택 테스트 실행까지 추가로 처리한다.
7. 단축키로 Java 개발하기
- Java 전용 키는
jdtls가 현재 Java 버퍼에 연결된 뒤에만 등록된다.
- Java 코드를 실제로 개발하려면 Java 전용 키뿐 아니라 Neovim의 파일, 버퍼, quickfix 기본 기능도 함께 사용한다.
7.1 파일과 버퍼 찾기
- 파일명 일부로 현재 프로젝트 아래의 Java 파일을 찾는다.
:find **/*UserService*.java<Tab>
- 프로젝트 전체에서 클래스명, 메서드명 또는 문자열을 검색한다.
:grep! UserService
:copen
| 단축키 또는 명령 |
기능 |
Enter |
quickfix에서 선택한 검색 결과 열기 |
]q |
다음 quickfix 결과 |
[q |
이전 quickfix 결과 |
:cclose |
quickfix 창 닫기 |
:buffers |
열려 있는 버퍼 목록 확인 |
]b |
다음 버퍼 |
[b |
이전 버퍼 |
Ctrl-^ |
현재 버퍼와 직전 버퍼 사이 전환 |
:bd |
현재 파일 버퍼 닫기 |
- 파일명은
:find, 파일 내용은 :grep!, 이미 연 파일은 버퍼 명령으로 찾는다고 구분한다.
7.2 자동완성 사용
- Insert 모드에서 코드를 입력하면
jdtls가 타입과 현재 문맥에 맞는 후보를 nvim-cmp로 전달한다.
| 단축키 |
기능 |
Ctrl-n 또는 Down |
다음 완성 후보 |
Ctrl-p 또는 Up |
이전 완성 후보 |
Enter |
선택한 후보 확정 |
Ctrl-e |
완성 목록 닫기 |
Ctrl-Space |
완성 목록 수동 호출 |
<C-Space>가 tmux의 <prefix>와 같은 경우 <prefix><prefix>로 Neovim에 해당 키를 전달한다.
- 평소에는 자동으로 열린 목록에서
Ctrl-n, Ctrl-p, Enter만 사용하면 된다.
7.3 정의와 참조를 따라 코드 읽기
| 단축키 |
기능 |
gd |
클래스, 메서드, 필드 정의로 이동 |
gr |
현재 심볼을 참조하는 위치 확인 |
gri |
현재 인터페이스나 메서드의 구현 목록 확인 |
gO |
현재 파일의 클래스와 메서드 심볼 확인 |
K |
타입, 문서, 메서드 시그니처 표시 |
Ctrl-o |
이동하기 전 위치로 돌아가기 |
Ctrl-i |
돌아오기 전 위치로 다시 이동 |
- 처음 보는 메서드 호출 위에서
gd를 눌러 구현으로 이동한다.
- 구현을 읽은 뒤
Ctrl-o로 호출부에 돌아온다.
- 이 메서드를 누가 사용하는지 확인할 때는
gr을 누른다.
- 참조 목록이 quickfix로 열리면
]q, [q, Enter로 각 호출 지점을 확인한다.
- 인터페이스의 실제 구현체를 찾을 때는 Neovim 0.11 기본 LSP 키인
gri를 사용한다.
7.4 진단을 확인하고 수정하기
| 단축키 |
기능 |
]d |
다음 오류 또는 경고 |
[d |
이전 오류 또는 경고 |
]D |
마지막 진단 |
[D |
첫 번째 진단 |
<leader>ld |
현재 위치의 진단 메시지를 팝업으로 표시 |
<leader>ca |
현재 위치에서 가능한 Code Action 확인 |
- 빨간 오류가 보이면
]d로 이동하고 <leader>ld로 전체 메시지를 읽는다.
- import 누락이나 자동 수정이 가능한 오류라면
<leader>ca를 열고 원하는 작업을 선택한다.
- 자동 수정 후에도 오류가 남아 있으면 다시
]d로 다음 진단을 확인한다.
7.5 리팩터링과 코드 정리
| 단축키 |
기능 |
<leader>rn |
클래스, 메서드, 필드, 변수 이름 변경 |
<leader>jo |
사용하지 않는 import 제거 및 import 정리 |
<leader>jf |
현재 Java 파일 포맷 |
- 심볼 이름을 프로젝트 전체에서 바꿀 때 문자열 치환보다
<leader>rn을 사용한다.
- 새 이름을 입력하고
Enter를 누르면 jdtls가 참조 위치까지 함께 변경한다.
- 코드 작성이 끝나면
<leader>jo로 import를 정리한 뒤 <leader>jf로 포맷한다.
7.6 테스트 범위를 점진적으로 넓히기
| 단축키 |
실행 범위 |
주로 사용하는 시점 |
<leader>jm |
커서가 들어 있는 테스트 메서드 |
구현 중 가장 자주 반복 |
<leader>jc |
현재 파일의 테스트 클래스 |
클래스 단위 기능 확인 |
<leader>jt |
프로젝트 전체 테스트 |
작업 마무리 전 회귀 확인 |
<leader>jb |
테스트를 제외한 프로젝트 빌드 |
패키징 가능 여부 확인 |
- 테스트 메서드의 선언부나 본문에 커서를 두고
<leader>jm을 누른다.
- 클래스명과 메서드명을 직접 입력할 필요가 없다.
- 현재 설정은
jdtls의 document symbol에서 커서가 포함된 메서드를 찾아 다음 명령으로 변환한다.
./mvnw -Dtest=SampleTest#createsUser test
- Gradle 프로젝트에서는 다음 형태로 변환한다.
./gradlew test --tests SampleTest.createsUser
- 선택 테스트를 실행하기 전에 현재 파일은 자동 저장된다.
<leader>jt와 <leader>jb는 열려 있는 수정 버퍼를 자동 저장하지 않으므로 실행 전 :wall로 모두 저장한다.
- 하나의 메서드가 통과하면
<leader>jc, 클래스가 통과하면 <leader>jt 순서로 범위를 넓힌다.
7.7 테스트 결과 확인 후 코드로 돌아오기
- 빌드와 테스트 결과는 화면 아래의 12줄 터미널 split에서 확인한다.
- 테스트 실행 직후에는 터미널 입력 모드이므로 다음 순서로 코드 편집창으로 돌아간다.
- 결과 창이 더 이상 필요하지 않으면 해당 창에서 닫는다.
- 실패한 파일로 바로 이동하는 자동 파싱 기능은 현재 없다. 실패 메시지의 클래스와 줄 번호를 보고
:find, :grep! 또는 기존 버퍼로 이동한다.
8. Spring Boot 프로젝트에서 사용하는 흐름
cd my-spring-project
nvim .
8.1 프로젝트를 처음 열었을 때
src/main/java 또는 src/test/java의 Java 파일을 연다.
- 첫 실행에서는
jdtls가 Maven 의존성과 소스를 인덱싱할 때까지 기다린다.
:LspInfo에서 현재 버퍼에 jdtls가 연결되었는지 확인한다.
- 연결되었다면
K, gd, 자동완성이 동작하는지 확인한다.
8.2 모르는 코드를 읽을 때
:find **/*이름*.java<Tab>으로 시작 파일을 연다.
- 호출하는 메서드에서
gd로 정의를 따라간다.
K로 타입과 문서를 확인한다.
- 인터페이스라면
gri로 구현체를 찾는다.
- 누가 호출하는지 궁금하면
gr로 참조를 확인한다.
- 확인이 끝나면
Ctrl-o를 반복해 처음 위치로 돌아온다.
8.3 기능을 구현하거나 수정할 때
- 자동완성에서
Ctrl-n, Ctrl-p로 후보를 고르고 Enter로 확정한다.
- 오류는
]d, [d로 이동하고 <leader>ld로 자세히 읽는다.
- 자동 수정이 가능하면
<leader>ca를 실행한다.
- 이름 변경은
<leader>rn으로 참조까지 함께 수정한다.
<leader>jo, <leader>jf로 import와 포맷을 정리한다.
8.4 테스트를 반복할 때
- 수정한 테스트 메서드 안에서
<leader>jm을 실행한다.
- 실패 내용을 확인한 뒤
Ctrl-\ Ctrl-n, Ctrl-w k로 코드로 돌아온다.
- 코드를 수정하고 다시
<leader>jm을 실행한다.
- 메서드가 통과하면
<leader>jc로 현재 테스트 클래스를 실행한다.
- 클래스가 통과하면
<leader>jt로 전체 테스트를 실행한다.
8.5 작업을 마무리할 때
]d를 반복해 남아 있는 진단을 확인한다.
<leader>jo, <leader>jf로 소스를 정리한다.
:wall로 수정한 모든 버퍼를 저장한다.
<leader>jt로 전체 테스트를 실행한다.
<leader>jb로 테스트를 제외한 패키징도 성공하는지 확인한다.
9. 문제 확인
9.1 Java 파일로 인식되는지 확인
9.2 jdtls 실행 가능 여부 확인
:echo executable('java'), executable('jdtls')
9.3 LSP 연결 확인
- 현재 버퍼에
jdtls가 연결되어 있어야 Java 전용 단축키가 생긴다.
9.4 프로젝트 root 확인
- 단독
.java 파일에서는 LSP를 시작하지 않는다.
- 상위 경로에
pom.xml, mvnw, build.gradle, gradlew, .git 등이 있는지 확인한다.
9.5 Treesitter 확인
:TSInstall java
:checkhealth vim.treesitter
- parser가 없더라도
jdtls LSP 자체는 실행할 수 있지만 Java 구문 강조 품질은 떨어질 수 있다.
10. 정리
- Java IDE 기능의 중심은
jdtls와 Neovim 내장 LSP다.
- Treesitter는 구문 강조, nvim-cmp는 자동완성 UI를 담당한다.
- Mason은 공통 도구 관리자이며 현재
jdtls 설치에는 사용하지 않는다.
- 평소에는
gd, gr, K, <leader>ca로 탐색하고 수정한다.
- 테스트 개발에서는
<leader>jm을 가장 자주 사용하고, 범위를 넓힐 때 <leader>jc, <leader>jt 순서로 사용한다.
- tmux 명령은
<prefix>, Neovim Java 명령은 <leader>로 구분한다.