고객사의 수많은 애플리케이션 배포 파이프라인을 하나로 추상화하는 작업을 하고 있다. 파이프라인은 하나로 모이지만 그 위를 지나가는 애플리케이션의 Java 버전은 제각각이라, 추상화한 파이프라인이 어느 버전에서도 빌드를 깨뜨리지 않는지 로컬에서 직접 돌려봐야 했다. 그런데 java -version을 치면 Corretto 21 하나만 나온다. 당장 필요한 건 JDK 11이었다.

이 상황에서 할 수 있는 가장 단순한 일은 JDK 11을 하나 더 설치하고 JAVA_HOME을 손으로 바꾸는 것이다. 그 방법은 두 번째 버전부터 무너진다. 테스트할 버전이 하나가 아니기 때문이다. 어느 터미널이 어느 JDK를 보고 있는지 매번 확인해야 하고 되돌리는 걸 잊으면 다음 날 21이 필요한 프로젝트가 이유 없이 깨진다.

JDK 전환은 설치의 문제가 아니라 PATH 우선순위의 문제다. 어떤 java가 실행되느냐는 설치된 JDK의 개수가 아니라 쉘이 PATH를 어느 순서로 훑느냐가 결정한다. SDKMAN은 그 순서에 끼어드는 도구다. SDKMAN이 기존 JDK를 지우지 않고 우선순위를 가져가는 방식, 이미 깔려 있던 JDK를 처리하는 방법, 빌드 테스트에 맞는 전환 방식까지 차례로 정리한다.

쉘이 java를 찾는 순서가 곧 현재 JDK다

터미널에서 java를 입력하면 쉘은 PATH에 등록된 디렉토리를 앞에서부터 순서대로 뒤져 가장 먼저 발견되는 java 실행 파일을 쓴다. Corretto 21이 나오는 이유는 .zshrc 어딘가에서 그 JDK 경로가 PATH에 잡혀 있기 때문이다. 다른 JDK를 쓰고 싶으면 그보다 앞에 새 경로를 두면 된다.

SDKMAN은 설치 과정에서 쉘 설정 파일 맨 아래에 초기화 스크립트 한 줄을 추가한다.

export SDKMAN_DIR="$HOME/.sdkman"
[[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]] && source "$HOME/.sdkman/bin/sdkman-init.sh"

이 스크립트는 터미널이 새로 열릴 때마다 실행되면서 SDKMAN이 관리하는 JDK 경로를 기존 경로보다 앞, 그러니까 PATH의 맨 앞에 놓는다. 그래서 그 뒤로는 SDKMAN이 지정한 java가 항상 먼저 발견된다.

SDKMAN은 기존 Java를 덮어쓰거나 삭제하지 않는다. 더 높은 우선순위로 가로챌 뿐이다. JDK를 포함한 모든 SDK는 홈 디렉토리 아래 ~/.sdkman/candidates/에 독립적으로 설치되고 기존에 설치된 Corretto-21.0.3.9.1은 원래 자리에 그대로 있다. JAVA_HOME 같은 환경변수도 SDKMAN이 함께 관리하므로 쉘 설정을 직접 고칠 일이 없다.

이 구조라서 실패해도 되돌리기 쉽다. 초기화 스크립트 한 줄을 지우면 PATH는 원래대로 돌아가고 기존 JDK가 다시 앞으로 나온다.

이미 깔려 있던 JDK는 어떻게 할 것인가

SDKMAN을 설치하고 나면 기존 JDK를 두고 세 가지 선택지가 생긴다.

  1. 삭제한다. brew로 설치했으면 brew uninstall, 직접 설치했으면 직접 지운다. 가장 깔끔하지만 그 JDK에 의존하던 IDE나 스크립트가 있는지 먼저 확인해야 한다.

  2. 그냥 둔다. SDKMAN이 우선순위를 가져갈 뿐 기존 JDK에는 영향이 없으므로 아무것도 안 해도 된다. 대신 sdk list에는 보이지 않고 SDKMAN으로 그 버전으로 되돌아갈 수도 없다.

  3. SDKMAN에 로컬 버전으로 등록한다. 기존 JDK를 SDKMAN의 관리 대상으로 넣어 새로 설치한 버전들과 같은 명령어로 전환할 수 있게 한다.

하나의 도구로 모든 JDK를 다루려면 세 번째가 맞다. 절차는 두 단계다.

먼저 설치된 JDK 목록과 경로를 확인한다.

/usr/libexec/java_home -V

여기서 Corretto 21의 경로를 얻는다. 예를 들면 /Users/jin/Library/Java/JavaVirtualMachines/corretto-21.0.3/Contents/Home이다. 이 경로를 식별자와 함께 SDKMAN에 등록한다.

# sdk install java [원하는 식별자] [JDK 경로]
sdk install java 21.0.3-corretto-local /Users/jin/Library/Java/JavaVirtualMachines/corretto-21.0.3/Contents/Home

이제 sdk list java에 21.0.3-corretto-local이 나타나고 SDKMAN으로 설치한 다른 버전과 똑같이 sdk use나 sdk default로 전환할 수 있다. 식별자 뒤에 -local을 붙여 두면 SDKMAN이 내려받은 배포판과 내가 직접 등록한 것을 목록에서 구분할 수 있다.

설치와 버전 전환

brew로도 설치할 수 있지만 공식 문서는 curl 스크립트를 권장한다. 이 방식이 위에서 본 초기화 스크립트를 쉘 설정에 자동으로 추가해 준다.

curl -s "https://get.sdkman.io" | bash

설치 가능한 버전은 sdk list java로 본다. OpenJDK, Oracle, Amazon Corretto, Zulu, Temurin 같은 배포판이 한 목록에 나오고, 식별자는 버전-배포판 형식이다.

# Temurin (구 AdoptOpenJDK) 21 버전 설치
sdk install java 21.0.4-tem

# Amazon Corretto 17 버전 설치
sdk install java 17.0.12-amzn

지금 필요한 것은 JDK 11이다.

sdk install java 11.0.28-amzn

설치한 뒤 전환하는 명령은 둘인데 그 차이가 이 글의 마지막 결정이다.

# 모든 새 터미널의 기본 JDK를 바꾼다
sdk default java 11.0.28-amzn

# 지금 열려 있는 이 터미널 세션에서만 바꾼다
sdk use java 11.0.28-amzn

빌드 테스트에는 default가 아니라 use다

sdk default는 기본 JDK를 통째로 바꾼다. 앞으로 여는 모든 터미널이 11을 본다. 반면 sdk use는 지금 열린 세션 하나에서만 11을 쓰고 터미널을 닫으면 원래 기본 버전으로 돌아간다.

특정 버전으로 빌드를 테스트하는 일은 예외 상황이지 새 기준이 아니다. 테스트할 버전이 여럿이면 더 그렇다. 예외를 기본값으로 만들면 다시 되돌리는 일이 생기고 그게 처음에 JAVA_HOME을 손으로 바꾸다 겪은 문제였다. 세션 단위 전환은 그 문제를 구조적으로 없앤다. 터미널을 열고, sdk use로 11을 잡고, 빌드를 돌리고, 닫는다. 다음 터미널은 아무 일도 없었다는 듯 21이다.

주의할 점이 하나 있다. sdk use는 그 쉘 세션의 PATH만 바꾼다. IDE는 보통 자기 설정에서 JDK를 따로 고르므로 터미널에서 11로 바꿨다고 IDE 빌드까지 11이 되지는 않는다. IDE에서 같은 프로젝트를 열 때는 프로젝트 SDK를 별도로 맞춰야 한다.

정리

  • 어떤 JDK가 실행되는지는 PATH 순서가 정한다. 설치 개수가 아니다.

  • SDKMAN은 기존 JDK를 지우지 않고 PATH 앞에 끼어든다. 초기화 스크립트 한 줄이 전부라서 되돌리기도 한 줄이다.

  • 기존 JDK는 SDKMAN에 로컬 버전으로 등록하는 편이 낫다. 도구 하나로 모든 버전을 같은 명령어로 다룰 수 있다.

  • **빌드 테스트에는 **sdk use를 쓴다. 세션이 끝나면 기본 버전으로 복귀하므로 예외가 기준이 되는 일이 없다.

  • sdk use는 터미널만 바꾼다. IDE의 프로젝트 SDK는 따로 맞춘다.

JDK를 바꾸는 도구는 많다. 어느 것을 고르든 결국 하는 일은 PATH 앞에 뭘 둘 것인가다. 그 원리를 알고 나면 도구가 무엇을 해 주고 무엇을 해 주지 않는지가 보인다.