[Vim_02] Java 개발 환경

VI-IF-02calendar_today2026-07-17 23:17#Vim #Java #SpringBoot

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로 관리한다.
shell
sdk list java
sdk install java

sdk list maven
sdk install maven

brew install jdtls
  • 프로젝트에 mvnw 또는 gradlew가 있으면 시스템 Maven/Gradle보다 wrapper를 우선 사용한다.
  • wrapper에 실행 권한이 없다면 권한을 추가한다.
shell
chmod +x mvnw
chmod +x gradlew
  • 설치 상태 확인
shell
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()를 직접 설정한다.
lua
{
  "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를 수동으로 관리할 때는 다음 명령을 사용한다.
vim
: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를 실행하지 않는다.
lua
{
  "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로 연결한다.
lua
{
  "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 실행 경로를 혼동하지 않는다.
lua
{ "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 파일의 상위 폴더에서 찾는다.
lua
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를 공유하면 인덱스가 충돌할 수 있으므로 프로젝트 경로를 해시하여 별도 폴더를 사용한다.
lua
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 등록과 자동 활성화

  • javajdtls가 모두 실행 가능하고 프로젝트 root가 발견되었을 때만 연결한다.
  • 단일 Java 파일 지원은 끈다.
lua
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 파일을 찾는다.
vim
:find **/*UserService*.java<Tab>
  • 프로젝트 전체에서 클래스명, 메서드명 또는 문자열을 검색한다.
vim
: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에서 커서가 포함된 메서드를 찾아 다음 명령으로 변환한다.
shell
./mvnw -Dtest=SampleTest#createsUser test
  • Gradle 프로젝트에서는 다음 형태로 변환한다.
shell
./gradlew test --tests SampleTest.createsUser
  • 선택 테스트를 실행하기 전에 현재 파일은 자동 저장된다.
  • <leader>jt<leader>jb는 열려 있는 수정 버퍼를 자동 저장하지 않으므로 실행 전 :wall로 모두 저장한다.
  • 하나의 메서드가 통과하면 <leader>jc, 클래스가 통과하면 <leader>jt 순서로 범위를 넓힌다.

7.7 테스트 결과 확인 후 코드로 돌아오기

  • 빌드와 테스트 결과는 화면 아래의 12줄 터미널 split에서 확인한다.
  • 테스트 실행 직후에는 터미널 입력 모드이므로 다음 순서로 코드 편집창으로 돌아간다.
text
Ctrl-\ Ctrl-n
Ctrl-w k
  • 결과 창이 더 이상 필요하지 않으면 해당 창에서 닫는다.
vim
:q
  • 실패한 파일로 바로 이동하는 자동 파싱 기능은 현재 없다. 실패 메시지의 클래스와 줄 번호를 보고 :find, :grep! 또는 기존 버퍼로 이동한다.

8. Spring Boot 프로젝트에서 사용하는 흐름

shell
cd my-spring-project
nvim .

8.1 프로젝트를 처음 열었을 때

  1. src/main/java 또는 src/test/java의 Java 파일을 연다.
  2. 첫 실행에서는 jdtls가 Maven 의존성과 소스를 인덱싱할 때까지 기다린다.
  3. :LspInfo에서 현재 버퍼에 jdtls가 연결되었는지 확인한다.
  4. 연결되었다면 K, gd, 자동완성이 동작하는지 확인한다.

8.2 모르는 코드를 읽을 때

  1. :find **/*이름*.java<Tab>으로 시작 파일을 연다.
  2. 호출하는 메서드에서 gd로 정의를 따라간다.
  3. K로 타입과 문서를 확인한다.
  4. 인터페이스라면 gri로 구현체를 찾는다.
  5. 누가 호출하는지 궁금하면 gr로 참조를 확인한다.
  6. 확인이 끝나면 Ctrl-o를 반복해 처음 위치로 돌아온다.

8.3 기능을 구현하거나 수정할 때

  1. 자동완성에서 Ctrl-n, Ctrl-p로 후보를 고르고 Enter로 확정한다.
  2. 오류는 ]d, [d로 이동하고 <leader>ld로 자세히 읽는다.
  3. 자동 수정이 가능하면 <leader>ca를 실행한다.
  4. 이름 변경은 <leader>rn으로 참조까지 함께 수정한다.
  5. <leader>jo, <leader>jf로 import와 포맷을 정리한다.

8.4 테스트를 반복할 때

  1. 수정한 테스트 메서드 안에서 <leader>jm을 실행한다.
  2. 실패 내용을 확인한 뒤 Ctrl-\ Ctrl-n, Ctrl-w k로 코드로 돌아온다.
  3. 코드를 수정하고 다시 <leader>jm을 실행한다.
  4. 메서드가 통과하면 <leader>jc로 현재 테스트 클래스를 실행한다.
  5. 클래스가 통과하면 <leader>jt로 전체 테스트를 실행한다.

8.5 작업을 마무리할 때

  1. ]d를 반복해 남아 있는 진단을 확인한다.
  2. <leader>jo, <leader>jf로 소스를 정리한다.
  3. :wall로 수정한 모든 버퍼를 저장한다.
  4. <leader>jt로 전체 테스트를 실행한다.
  5. <leader>jb로 테스트를 제외한 패키징도 성공하는지 확인한다.

9. 문제 확인

9.1 Java 파일로 인식되는지 확인

vim
:set filetype?
  • filetype=java가 나와야 한다.

9.2 jdtls 실행 가능 여부 확인

vim
:echo executable('java'), executable('jdtls')
  • 둘 다 1이어야 한다.

9.3 LSP 연결 확인

vim
:LspInfo
  • 현재 버퍼에 jdtls가 연결되어 있어야 Java 전용 단축키가 생긴다.

9.4 프로젝트 root 확인

  • 단독 .java 파일에서는 LSP를 시작하지 않는다.
  • 상위 경로에 pom.xml, mvnw, build.gradle, gradlew, .git 등이 있는지 확인한다.

9.5 Treesitter 확인

vim
: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>로 구분한다.