Jackson으로 파싱한 JSON 속성값을 생성자로 전달하기

Jackson으로 JSON을 파싱한 속성값을 객체의 생성자로 전달할 수 있는 여러가지 방법을 정리했습니다.

정상혁정상혁

1. Jackson에서 (no Creators, like default construct, exist) 에러 메시지

파싱하고자하는 JSON
{
    "accessDateTime": "2019-10-10T11:14:16Z",
    "ip": "175.242.91.54",
    "username": "benelog"
}

위와 같은 JSON을 파생해서 아래와 같이 setter가 없는 객체에 집어 넣고 싶은 경우가 있습니다.

파싱한 결과를 넣을 클래스
public class AccessLog {
    private final Instant accessDateTime;
    private final String ip;
    private final String username;

    public AccessLog(Instant accessDateTime, String ip, String username) {
        this.accessDateTime = accessDateTime;
        this.ip = ip;
        this.username = username;
    }

    public Instant getAccessDateTime() {
        return accessDateTime;
    }

    public String getIp() {
        return ip;
    }

    public String getUsername() {
        return username;
    }
}

Jackson 라이브러리로 JSON을 파싱하는 테스트 코드를 아래처럼 작성했습니다.

테스트 코드
class ConstructorPropertiesTest {
    @Test
    void parse() throws JsonProcessingException {
        var json = """
            {
            "accessDateTime": "2019-10-10T11:14:16Z",
            "ip": "175.242.91.54",
            "username": "benelog"
            }
            """;

        var objectMapper = new ObjectMapper()
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .registerModule(new JavaTimeModule());

        AccessLog accessLog = objectMapper.readValue(json, AccessLog.class);

        then(accessLog.getAccessDateTime()).isEqualTo("2019-10-10T11:14:16Z");
        then(accessLog.getIp()).isEqualTo("175.242.91.54");
        then(accessLog.getUsername()).isEqualTo("benelog");;
    }
}

위의 코드를 실행하면 다음의 Exception이 떨어집니다. (no Creators, like default construct, exist) 이 핵심적인 메시지입니다.

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `net.benelog.jackson.ConstructorPropertiesTest$AccessLog` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (String)"{
"accessDateTime": "2019-10-10T11:14:16Z",
"ip": "175.242.91.54",
"username": "benelog"
}
"; line: 2, column: 1]

	at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
	at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1592)
	at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1058)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1297)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:326)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4218)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3214)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3182)

JSON을 파싱한 결과를 전달할 적절한 생성자를 찾지 못했을 때 발생하는 에러입니다. 이 문제를 해결하는 방법을 정리합니다.

2. 생성자로 JSON 속성값을 전달하는 방법들

2.1. @JsonCreator

Jackson에서 제공하는 @JsonCreator, @JsonProperty 를 값을 전달할 생성자와 메서드 파라미터에 붙입니다.

AccessLog의 생성자에 @JsonCreator 선언
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

public class AccessLog {

    // 멤버 변수 선언 생략

    @JsonCreator
    public AccessLog(
        @JsonProperty("accessDateTime") Instant accessDateTime,
        @JsonProperty("ip") String ip,
        @JsonProperty("username") String username) {

        this.accessDateTime = accessDateTime;
        this.ip = ip;
        this.username = username;
    }

    // getter 생략
}
  • 장점

    • JSON의 속성명과 객체의 멤버변수명이 다를 때도 자연스럽게 활용할 수 있습니다.

    • 생성자가 에러 개 일때 Jackson에서 사용할 생성자를 명시적으로 지정할 수 있습니다.

  • 단점

    • Jackson에 의존적인 방법입니다.

      • Jar파일로 배포하는 클래스 안에서 이 방법을 사용하려면 Jackson에 대한 의존성이 추가됩니다.

      • JSON 파싱 라이브러리를 교체한다면 전체 클래스를 수정해야 합니다.

2.2. @ConstructorProperties

JDK 1.6부터 제공되었던 @java.beans.ConstructorProperties 은 생성자의 파라미터 이름을 지정하는 표준적인 방법입니다. 이를 활용하면 생성자의 파라미터 이름을 Reflection API를 통해서 알 수 있습니다. Jackson은 2.7.0버전부터 @ConstructorProperties 를 인지합니다. ( https://github.com/fasterxml/jackson-databind/issues/905 참조)

생성자에 @ConstructorProperties 으로 파라미터의 이름을 지정하면, Jackson에서는 동일한 이름의 JSON솔성값을 생성자로 넘겨줍니다.

AccessLog의 생성자에 `@ConstructorProperties`로 속성명 지정
import java.beans.ConstructorProperties;

public class AccessLog {

    // 멤버 변수 선언 생략

    @ConstructorProperties({"accessDateTime", "ip", "username"})
    public AccessLog(Instant accessDateTime, String ip, String username) {
        this.accessDateTime = accessDateTime;
        this.ip = ip;
        this.username = username;
    }

    // getter 생략
}

Lombok을 활용한다면 이 과정을 더 편하게 할 수 있습니다. lombok.config 를 다음과 같은 선언을 하면 Lombok에서 만드는 생성자에서 @ConstructorProperties 를 자동으로 넣어줍니다.

lombok.config 설정
lombok.anyConstructor.addConstructorProperties=true

@Builder, @AllArgsConstructor 와 같은 애노테이션을 클래스에 붙이면 Lombok에서는 자동으로 생성자를 만들어줍니다. 이를 통해 JSON 파싱한 값을 넣을 클래스를 더 단순하게 만들 수 있습니다.

Lombok을 이용한 AccessLog 클래스 선언
@Builder
@Getter
@ToString
public class AccessLog {
    private final Instant accessDateTime;
    private final String ip;
    private final String username;
}

참고로 Lombok v1.16.20 전까지는 디폴트로 @ConstructorProperties 을 넣어줬었다고 합니다. 이 이후 버전부터는 디폴트가 아니므로 lombok.config 에 명시적인 선언이 필요합니다. ( https://multifrontgarden.tistory.com/222 참조 )

@ConstructorProperties 를 직접 쓸 때의 장단점은 다음과 같다고 생각합니다.

  • 장점

    • @JsonCreator + @JsonProperties 보다는 코딩량이 조금 적습니다.

    • Jackson에 의존적이지 않습니다.

      • JSON을 파싱한 값이 들어가는 클래스를 jar 파일로 배포할 때 Jackson의 의존관계가 딸려들어가지 않습니다.

      • 같은 방식을 지원하는 다른 JSON 파싱 라이브러리로 교체할 때 코드 변경이 없습니다.

  • 단점

    • JSON의 속성명과 생성자의 실제 파라미터 명이 다른 경우에는 사용하는 것이 부자연스럽습니다.

만약 아래와 같이 @ConstructorProperties 에서는 "ip_address"로 지정한 속성이 실제 파라미터이름이 String ip 경우라면, 코드로는 잘 동작하지만 애노테이션의 원래 의도하는 어긋난 것이 아닌가 하는 생각이 들었습니다.

    @ConstructorProperties({"accessDateTime", "ip_address", "username"})
    public AccessLog(Instant accessDateTime, String ip, String username) {
        this.accessDateTime = accessDateTime;
        this.ip = ip;
        this.username = username;
    }

@ConstructorProperties + Lombok 은 코드량이 적다는 장점이 있지만 멤버 변수의 이름이 JSON 속성명과 일치해야 한다는 단점도 있습니다. jar 파일로 배포하는 클래스라면 Lombok에 대한 의존성이 부담스러울수도 있습니다.

2.3. ParameterNameModule 활용

앞의 예제들을 보면 @JsonProperty("ip") 와 같이 지정하는 속성의 이름과 생성자의 파라미터의 이름이 동일합니다. String ip 와 같이 생성자의 파라미터의 이름을 바로 가지고 올 수 있다면 일일히 속성명을 지정하지 않을 수 있겠다는 생각이 들만합니다.

그런데 JDK 8이 나오기 전까지는 Reflection만으로는 파라미터 이름을 가지고 올 수 없었고, ASM과 같은 바이트코드 조작 라이브러리를 이용해서 디버깅을 위한 정보를 이용해야만 가능했습니다. ( https://stackoverflow.com/questions/2729580/how-to-get-the-parameter-names-of-an-objects-constructors-reflection#2729907 참조) 그래서 앞서 소개한 @java.beans.ConstructorProperties 와 같은 애노테이션도 활용되었습니다.

JDK8 이상에서는 컴파일을 할 때 -parameters 라는 옵션을 붙이면 Reflection API로 파라미터 정보를 가지고 올수 있도록 컴파일된 클래스에 정보를 덧붙여 줍니다. Gradle을 쓰고 있다면 아래와 같이 설정할 수 있습니다.

build.gradle 안의 컴파일 옵션에 추가
tasks.withType(JavaCompile).each {
    it.options.compilerArgs.add('-parameters')
}

IDE 안에서도 컴파일 옵션을 신경써줘야합니다.

IntelliJ에서는 Settings > Build, Execution, Development > Build Tools > Gradle 에서 Build and Run using: 옵션을 확인해 봅니다.

intellij-settings-gradle.png

이 옵션값이 Gradle(Default)`로 되어 있다면, `build.gradle 의 컴파일 옵션이 그대로 쓰입니다. 만약 그 값이 IntelliJ IDEA 로 되어 있다면 IntelliJ 안에서의 Java 컴파일 옵션도 동일하게 맞춰 줘야합니다.

Settings > Build, Execution, Development > Compiler > Java Compiler 메뉴에서 Addtional command line parameters 옵션에 -parameters 을 적어줍니다. 옵션을 바꾼 후에는 전체 프로젝트를 리빌드합니다. ( Build > Rebuild Project )

intellij-settings-java-compiler.png

Jackson의 ParameterNameModule 을 쓰기 위해서는 다음과 같이 의존성을 추가해야합니다.

ParameterNameModule 의존성 추가
    implementation 'com.fasterxml.jackson.module:jackson-module-parameter-names:2.10.3'

ObjectMapper 선언에서는 registerModule() 메서드로 ParameterNamesModule 을 추가합니다.

ObjectMapper에 ParameterNamesModule 추가
    var objectMapper = new ObjectMapper()
        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
        .registerModule(new JavaTimeModule())
        .registerModule(new ParameterNamesModule());

이렇게 하면 생성자에 특별한 애너테이션을 붙이지 않아도 Jackson은 JSON의 속성을 생성자에게 전달됩니다.

Spring Boot에서는 ParameterNamesModule 을 편하게 쓸 수 있도록 아래와 같은 기본 설정이 제공됩니다.

  • Spring Boot Gradle Plugin에서 Java 컴파일의 -parameters 옵션이 자동 추가됩니다.

  • spring-boot-starter-web 에서 이미 jackson-module-parameter-names 에 대한 의존성이 추가되어 있습니다.

  • 디폴트로 등록되는 ObjectMapper bean에는 ParameterNamesModule 이 이미 추가되어 있습니다.

    • JacksonAutoConfiguration.java#L108 참조

    • RestTeamplteBuilderRestTemplate 을 생성한다면 디폴트 등록된 ObjectMapper 을 참조하는 MappingJackson2HttpMessageConverterRestTemplate 에 주입됩니다.

ParameterNamesModule 은 Lombok에서 자동으로 만든 생성자도 잘 인식합니다. lombok.config 에 추가 설정을 하지 않아도 된다는 점이 @ConstructorProperties 를 쓸 때와의 차이점입니다.

이 방식의 장단점은

  • 장점

    • 코드가 짧습니다.

    • Jackson에 대한 의존성이 없습니다.

  • 단점

    • 생성자의 파라미터명과 JSON 속성의 이름이 반드시 일치해야 합니다.

      • 생성자의 파라미터 이름이 JSON파싱에 쓰인다는것을 의식하지 않는다면, 파라미터 명을 잘 모르고 고쳐서 JSON 파싱이 안되게 하는 부작용이 쓰일수 있습니다.

    • 컴파일 옵션을 의식하지 않으면 특정 개발자의 IDE에서는 의도대로 동작하지 않을수 있습니다.

    • 생성자가 여러 개 일때는 @JsonCreator 와 같은 다른 방식과 병행해서 써야 합니다.

3. 예제 소스 저장소

예제는 https://github.com/benelog/jackson-expriment 에 올려두었습니다.

Text blocks 문법을 활용하려고 JDK 13을 쓴 예제입니다. InteliJ 안에서 경고가 뜬다면 'Set Language level to 13(Preview)' 를 선택해줍니다.

text-blocks.png

ktlint로 Kotlin 공식 코딩 컨벤션 맞추기

Kotlin 언어에는 공식 코딩 컨벤션이 정의되어 있습니다. 이를 준수할수 있도록 Gradle 빌드에서 ktlint로 코드 스타일을 검사하고, IntelliJ의 포멧터, Git의 pre-commit hook과 연동하는 방법을 안내합니다.

정상혁정상혁

Kotlin 언어의 공식 사이트에서는 코딩 컨벤션 가이드를 제공합니다.

ktlint는 Kotlin의 공식 가이드의 규칙을 포함하여 코드 스타일을 검사하고, 맞춰주는 도구입니다. 이 글에는 ktlint를 Gradle, IntelliJ, Git과 연동하여 사용하는 방법을 정리했습니다.

1. Gradle 빌드 설정

ktlint는 jar 파일로 제공되고, https://jcenter.bintray.com/ 에도 배포되어 있습니다. 따라서 별도의 Gradle plugin이 없어도 Gradle에서 task를 정의해서도 쓸 수 있습니다. 그런데 ktlint의 공식 사이트에서는 Gradle plugin을 더 권장한다고 나와 있습니다. 이 글에서는 ktlint를 래핑한 Gradle plugin 중 Github에서 별 갯수가 가장 높은 jlleitschuh/ktlint-gradle를 사용했습니다.

1.1. .editorconfig 설정

ktlint는 .editorconfig 파일에 선언된 규칙을 포함하여 코드 스타일을 검사합니다. .editorconfig 는 다양한 에디터와 IDE에서 공통적으로 지원하는 코드 스타일에 대한 설정 파일입니다. 자세한 스펙은 https://editorconfig.org/ 에서 파악할 수 있습니다.

아래와 같이 .editorconfig 설정을 추가하면 Kotlin 코딩컨벤션과 함께 쓰기에 무난합니다.

.editorconfig 예시
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 120
tab_width = 4

[*.{kt,kts}]
disabled_rules=import-ordering
# java.* 패키지를 의존하는 경우  IntelliJ의 Orgarnize Import 기능으로는 알파벳 순서대로 import 구문을 정렬할 수 없다.
# 이는 ktlint의 import-ordering 규칙과 맞지 않는다.

ktlint는 .editorconfig 파일이 없어도 디폴트 규칙으로 실행할 수 있습니다. 그렇지만 아래와 같은 이유로 명시적으로 이 파일을 선언하는 것을 권장합니다.

  • 의도하지 않게 상위 디렉토리의 .editorconfig 가 참조되는 경우를 막아줍니다.

    • .editorconfig 파일의 규약에 따르면, 이 파일을 지원하는 도구들은 root = true 라는 선언이 된 .editorconfig 파일을 찾기 전까지는 모든 상위 디렉토리를 탐색합니다. [1]

    • ktlint에서도 이 규약을 준수합니다. 따라서 특정 컴퓨터에만 소스를 복제한 디렉토리의 상위 디렉토리에 .editorconfig 가 있다면 그 장비에서는 스타일 체크 결과가 다르게 나오게 됩니다.

  • ktlint가 버전이 올라가면서 규칙의 디폴트 값이 변경될 경우를 대비합니다.

    • 예를 들면 ktlint 0.34.0 버전부터 insert_final_newline = true 가 디폴트 값이 되었습니다. [2]

  • 다양한 IDE와 Editor등의 도구에서 이 설정을 참조합니다.

    • IntelliJ, VSCode, GitHub등에서 뷰어,포멧터의 설정으로 자동 반영됩니다.

  • 코드 포멧에 대한 문서와 역할도 할 수 있습니다.

    • 위의 예시와 같이 .editorconfig 에 선언되는 속성의 이름은 이해하기 쉽습니다. 이 파일만 봐도 파일 포멧에 대한 일부 규칙을 파악할 수 있습니다.

  • Kotlin 공식 코딩 컨벤션에 명시되지 않은 규칙까지 일치시킬 수 있습니다.

    • max_line_length , insert_final_newline 과 같은 규칙을 Kotlin 공식 코딩 컨벤션에는 명시되어 있지는 않습니다. 그러나 같은 소스를 고치면서 협업하는 개발자들이 이를 통일하지 않을 경우 불필요한 diff 가 발생하여 코드 변경분을 파악할 때 불편해집니다.

  • disabled_rules 속성으로 검사하지 않을 규칙을 지정할 수 있습니다.

    • 특히 현시점에서는 import-ordering 규칙은 쓰지 않는 것이 좋습니다. 위의 예시 파일에도 이를 반영했습니다.

jlleitschuh/ktlint-gradle는 아래와 같이 설정합니다.

Gradle Groovy설정 (build.gradle)
buildscript {
    repositories {
        maven {
            url 'https://plugins.gradle.org/m2/'
        }
    }
    dependencies {
        classpath 'org.jlleitschuh.gradle:ktlint-gradle:9.1.0'
    }
}

plugins {
    id 'org.jlleitschuh.gradle.ktlint' version '9.1.0'
}
Gradle Kotlin설정 (build.gradle.kts)
buildscript {
    repositories {
        maven(url = "https://plugins.gradle.org/m2/")
    }
    dependencies {
        classpath("org.jlleitschuh.gradle:ktlint-gradle:9.1.0")
    }
}
plugins {
    id("org.jlleitschuh.gradle.ktlint") version "9.1.0"
}
Caution

jlleitschuh/ktlint-gradle는 Gradle 버전 5.4.1이상에서만 사용할 수 있습니다. 그 이하 버전에서는 아래와 같은 에러가 나옵니다.

* What went wrong:
Could not determine the dependencies of task ':ktlintCheck'.
> Could not create task ':ktlintTestSourceSetCheck'.
   > Could not create task of type 'KtlintCheckTask'.
      > Could not generate a decorated class for class org.jlleitschuh.gradle.ktlint.KtlintCheckTask.
         > org/gradle/work/InputChanges

이럴 경우에는 Gradle의 버전을 업그레이드해야합니다.

Gradle Wrapper를 쓰고 있다면 {프로젝트홈}/gradle/wrapper/gradle-wrapper.properties 파일의 distributionUrl 속성에 지정된 버전을 고쳐서 버전을 올릴 수 있습니다.

distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-bin.zip

다양한 Gradle 버전을 별도로 설치하는데에는 SDKMAN 을 권장합니다. Gradle이 별도로 설치되어 있을 경우 Gradle Wrapper를 업그레이드하는 방법은 https://java.ihoney.pe.kr/476 을 참조합니다.

Gradle 버전을 업그레이드하기가 어려울 경우 https://ktlint.github.io/ 에 안내된 다른 Gradle Plugin이나 Plugin없이 사용하는 방법을 참고하시기 바랍니다.

gradle.properites 파일에도 공식 코딩 컨벤션을 사용한다는 정책을 아래와 같이 명시합니다.

gradle.properites 설정
kotlin.code.style=official

IntellJ에서 프로젝트를 import할때 위의 설정을 참조할 수 있습니다.

플러그인 설정이 완료되면 ktlint를 실행하는 Gradle 태스크는 ./gradlew tasks | grep ktlint 명령으로 확인합니다.

1.2. 스타일 검사

./gradlew ktlintCheck 태스크는 스타일 검사를 수행합니다. 이 태스크는 ./gradlew build 를 실행했을 때 연결되는 전체 프로젝트 빌드 싸이클에 포함됩니다. 따라서 디폴트 설정으로는 ktlint에서 가이드하는 코드 스타일을 지키지 않으면 빌드가 실패합니다.

1.3. 스타일 일괄 변환

./gradlew ktlintFormat 태스크로는 스타일에 맞지 않는 코드를 바꿔줍니다. 전체 프로젝트의 소스 코드를 한꺼번에 고칠 때 사용할 수 있습니다.

그런데, 이 기능을 사용하다가 의도하지 않게 파일이 삭제되는 경우도 있었습니다. 그래서 이 태스크는 조심해서 사용해야합니다. 이 글에서 설명한 IntelliJ로 포멧을 일괄변환하는 방법도 참고해서 병행해서 사용하는 편이 좋습니다.

1.4. Git hook으로 설정

앞에서 설명한 ktlintCheck , ktlintFormat 태스크를 Git의 Hook으로 등록하여 commit을 하면 자동으로 실행되게 할 수 있습니다.

  • ./gradlew addKtlintCheckGitPreCommitHook : ktlintCheck 태스크를 pre-commit hook으로 등록

  • ./gradlew addKtlintFormatGitPreCommitHook : ktlintFormat 태스크를 pre-commit hook으로 등록ktlintCheck 태스크로 검사하도록 설정합니다.

앞에서 설명한 것처럼 ktlintFormat 태스크는 의도하지 않게 파일을 바꿀 위험이 있기 때문에 addKtlintCheckGitPreCommitHook 를 더 권장합니다.

등록된 pre-commit hook은 rm .git/hooks/pre-commit 명령으로 삭제할 수 있습니다.

2. IntelliJ 설정

IntelliJ의 코드 포멧터는 Kotlin 공식 코딩 컨벤션이 나오기 전부터 쓰이던 디폴트 설정이 있었습니다. IntelliJ에서는 Kotlin 1.3에서는 신규로 생성되는 프로젝트에서, Kotlin 1.4에서는 모든 프로젝트에서 공식 컨벤션에 맞춘 포멧터가 디폴트로 설정될 것이라고 합니다. [3] 오랫동안 유지보수할 Kotlin 코드라면 공식 컨벤션에 맞춰서 IntelliJ에서도 포멧터를 설정되었는지 신경을 써야 합니다.

2.1. 포멧터 설정

다음에 안내한 A, B 2가지 방법 중의 하나를 선택하셔서 공식 코딩컨벤션에 맞게 IntelliJ의 포멧터 설정을 맞출수 있습니다.

2.1.1. A. `predefined ` 을 이용한 설정

  1. IntelliJ 메뉴에서 Settings > Editor > Code Style > Kotlin 으로 이동합니다.

  2. Scheme 항목을 Project로 지정합니다.

    • 여러 프로젝트에서 다른 설정을 쓸 경우를 대비해 가급적 글로벌 설정을 바꾸지 않기 위함입니다.

  3. Set from…​Predefined StyleKotlin Style Guide 를 선택합니다.

IntelliJ Kotlin Style

2.1.2. B. Gradle plugin을 이용한 설정

앞서 설정한 jlleitschuh/ktlint-gradle를 이용하여 ./gradlew ktlintApplyToIdea 태스크를 실행합니다. 이 태스크는 IntelliJ 설정파일의 코드 스타일 부분을 덮어써서 ktlint의 규칙과 가급적 맞는 포멧터가 설정합니다.

포멧터가 설정되면 파일을 편집할 때 Code > Reformat Code 메뉴를 선택하거나 단축키 Ctrl + Shift + L 단축키로 포멧터를 적용할 수 있습니다.

2.2. 기존 코드 일괄 변환

  1. IntelliJ의 프로젝트 탐색기에서 프로젝트의 최상위 디렉토리를 우클릭합니다.

  2. Reformat Code 를 실행합니다.

    • 우클릭을 하여 나오는 메뉴에서 Reformat Code 를 선택하거나

    • Ctrl + Shift + L 단축키를 누릅니다.

2.3. 파일을 저장할 때마다 포멧터 적용하기

Save Actions Plugin를 사용하면 파일을 저장할 때 자동으로 포멧터를 실행할 수 있습니다.

  1. File > Settings ( Ctrl + Alt + S ) > Plugins 메뉴로 이동합니다.

  2. Marketplace 탭에서 'Save Actions' 로 검색합니다.

  3. Save Actions' plugin의 상세 설명 화면에서 `[Install] 버튼을 누릅니다.

  4. IntelliJ를 재시작합니다.

  5. File > Settings > Other Settions > Save Actions 메뉴로 이동합니다.

  6. 아래 항목 혹은 그외의 원하는 정책을 체크합니다.

    • Activate save actions on save

    • Optimize imoprts

    • Refomat file (전체 프로젝트의 스타일이 통일된 경우)

    • Refomat only changed code (프로젝트의 스타일이 통일되어 있지 않아서 스타일이 맞지 않는 코드를 함께 고치면 변경 부분을 알아보기가 더 어려운 경우)

IntelliJ Save Actions

2.4. 포멧터가 바꿔주지 않는 규칙 신경쓰기

IntelliJ의 포멧터는 ktlint에서 검사하는 규칙을 다 자동을 맞추어주지는 못합니다. 즉 Reformat Code ( Ctrl + Alt + L ) 을 실행하는 것만으로는 ktlint 검사를 통과한다는 보장은 없습니다. 다음에서 설명하는 IntelliJ의 기능을 잘 활용하면 보다 빠른 시점에서 ktlint의 규칙을 준수하는데 도움이 됩니다.

2.4.1. 파일의 마지막에 자동으로 개행문자 추가

POSIX 명세에 따라서, 텍스트 파일의 마지막에 개행문자(LF)를 추가하는 것이 권장됩니다. [4]

그런데, IntelliJ의 Reformat Code 기능으로는 마지막 개행문자 추가가 되지 않습니다. File > Settings > Editor > General 메뉴에서 Ensure line feed at file end on Save 를 선택하면, 파일이 저장될 때 자동으로 마지막에 개행문자를 추가해줍니다.

IntelliJ line feed end of file

이 설정은 .editorconfig 의 선언에 따라 자동으로 활성화될 수도 있습니다. 그렇지만 의도대로 동작하지 않는다면 한번 확인해볼만합니다.

2.4.2. IntelliJ 경고에 따라 고치기

Kotlin 공식 코딩 컨벤션에는 공백 등 단순한 파일 형식 외에도 문법적인 요소에 대한 것도 있습니다.

예를 들면 String Template를 쓸 때는 꼭 필요한 경우가 아니면 중괄호를 넣지 말라는 규칙이 있습니다. ( https://kotlinlang.org/docs/reference/coding-conventions.html#string-templates )

  • (O): println("$name is my friend.")

  • (X): println("${name} is my friend.")

이 규칙을 어긴 코드는 'Reformat Code' 로는 바로 바뀌지 않습니다. IntelliJ에서 이런 코드에 경고를 보내고 Alt + Shift + Enter 단축키로 코드로 바꾸는 기능을 제공합니다.

IntelliJ String template

이처럼 IntelliJ에서는 언어 문법을 활용할때도 Kotlin 공식 코딩 컨벤션에서 권장하는 스타일대로 쓰도록 유도하고 있습니다. 이런 경고들을 무시하지 않고 반영한다면 ktlint의 검사에서도 통과할 가능성이 높아집니다.


1. https://editorconfig.org/ 에서 'When opening a file, EditorConfig plugins look for a file named .editorconfig in the directory of the opened file and in every parent directory.'

여러 개의 JDK를 설치하고 선택해서 사용하기

하나의 개발 장비에 여러 배포판/버전의 JDK를 설치하고 선택해서 사용할 때 편하게 쓸 수 있는 도구들을 소개합니다.

정상혁정상혁

다양한 배포판과 버전의 JDK를 명령어 한 줄로 설치하고 OS의 쉘에서 사용할 JDK를 쉽게 지정할 수 있게 해주는 도구들을 소개합니다.

주요 변경이력
  • 2019.07.03

    • Homebrew에 대한 소개를 별도의 단락으로 분리

    • Chocolatey에 대한 설명 보강

1. 특별한 도구를 안 쓸 때의 JDK 설치 & 버전 선택

1.1. JDK 설치

JDK를 수동으로 설치하는 절차는 아래와 같습니다.

  1. 설치할 버전/배포판을 다운로드합니다.

  2. 다운로드한 파일의 압축을 풉니다.

  3. OS의 환경변수를 지정합니다.

    • JAVA_HOME

      • JDK의 압축을 푼 디렉토리를 지정합니다.

      • Maven이나 Tomcat 같은 솔류션에서 이 환경변수로 JDK의 위치를 참조합니다.

    • PATH

      • 쓰고 있던 PATH 변수에 $JAVA_HOME/bin 을 더합니다.

      • java , javac 등을 명령행에서 직접 실행할 수 있도록 하기 위해서 하는 작업입니다.

1.2. JDK 버전 선택

IDE에서는 프로젝트별로 사용할 JDK의 위치를 선택할 수 있습니다. IntelliJ에서는 File > Project Structure > Platform Settings (단축키 Ctrl + Alt + Shift + S ) > SDK 메뉴 에서 이를 지정합니다.

:intelli-j-jdk.jpg

OS의 명령행에서 Maven, Gradle로 직접 빌드를 하거나 java -jar 로 직접 프로그램을 실행시킬 때를 대비해서 JAVA_HOME, PATH 설정이 되어 있어야합니다. 프로젝트마다 사용하는 JDK 버전이 다르면 사용할 JDK를 지정하기가 번거롭습니다. 매번 이런 JAVA_HOME 같은 환경 변수를 바꾸거나 /usr/lib/jvm/java-13-openjdk-amd64/bin/java 와 같이 전체 경로로 실행할 도구를 지정한다면 더욱 그렇습니다.

반복적인 작업을 쉡스크립트나 배치파일로 할 수도 있습니다. 그런데 이미 이런 작업을 편리하게 해주는 도구들이 몇 가지 있습니다.

2. 다양한 JDK 설치와 사용을 편하게 하는 도구

JDK의 설치와 OS의 명령행에서 사용할 버전을 지정할 때는 아래 도구들을 사용할 수 있습니다.

Table 1. JDK 설치와 버전 지정에 사용할 수 있는 도구들
이름 기능 사용 가능한 OS

YUM/APT

범용 패키지 관리 도구

Linux

update-alternatives/alternatives

범용 패키지 버전 선택 도구

Linux

Homebrew

범용 패키지 관리 도구

macOS

Chocolatey

범용 패키지 관리 도구

Windows

SDKMAN

범용 패키지 관리 도구

Linux
macOS
Windows(Cygwin, Git Bash)

jabba

JDK 설치 특화 도구

Linux
macOS
Windows

jEnv

JDK 버전 선택 특화 도구

Linux
macOS

direnv

범용 디렉토리별 환경 변수 관리 도구

Linux
macOS
Windows

2.1. APT/YUM

Ubuntu, CentOS 등의 Linux 배포판에서는 해당 OS에 맞도록 빌드한 OpenJDK 배포판을 APT,YUM 등으로 간단히 설치할 수 있도록 제공합니다.

APT(Advanced Packaging Tool)는 Ubuntu 등 Debian 계열의 리눅스에서 사용할 수 있는 패키지 관리 프로그램입니다. Ubuntu에서는 APT로 아래와 같이 여러 버전의 JDK를 설치할 수 있습니다.

sudo apt install openjdk-8-jdk
sudo apt install openjdk-11-jdk
sudo apt install openjdk-12-jdk

YUM(Yellow dog Updater, Modified)은 Red Hat/CentOS 리눅스 배포판에서 사용할 수 있는 패키지 관리자입니다. 아래와 같이 사용할 수 있습니다.

sudo yum install java-1.8.0-openjdk-devel.x86_64

Adopt OpenJDK 배포판은 패키지 저장소를 추가해서 설치할 수 있습니다. Ubuntu에서는 아래와 같이 합니다.

sudo add-apt-repository ppa:rpardini/adoptopenjdk
sudo apt install adoptopenjdk-11-installer

설치된 JDK의 java , javac 도구는 /usr/bin/java , /usr/bin/javac 에서 심볼릭 링크로 연결되어 어느 디렉토리에서나 실행될 수 있게 됩니다. 이 심볼릭 링크는 이어서 소개할 update-alternatives / alternatives 도구로 관리할수 있습니다. JAVA_HOME 환경 변수는 직접 ~/.bashrc 와 같은 쉘별 설정 파일에 넣어줘야 합니다.

  • 장점

    • OS에서 기본 제공하는 도구이기에 도구를 위한 별도의 설치 과정이 필요 없습니다.

    • JDK 외에도 Maven, Gradle의 설치에도 활용할 수 있는 범용적인 패키지 관리 도구입니다.

  • 단점

    • SDKMAN/ Jabba에 비하면 다양한 JDK 배포판을 제공하지는 않습니다.

2.2. update-alternatives / alternatives

update-alternatives와 alternatives는 여러 버전의 패키지를 관리할 수 있는 Linux에서 제공되는 도구입니다. 여기서는 Ubuntu에서 쓰는 update-alternatives 를 기준으로 설명하겠습니다.

앞서 나온데로 apt 로 설치한 JDK는 /usr/bin/java 에서 심볼릭 링크로 연결됩니다. 이 심블릭 링크는 /etc/alternatives/java 를 중간에 거쳐서 실제 설치한 디렉토리로 연결된 다는 것을 아래와 같이 확인할 수 있습니다.

➜  ~ ll /usr/bin/java
lrwxrwxrwx 1 root root 22  6월  9 22:20 /usr/bin/java -> /etc/alternatives/java
➜  ~ ll /etc/alternatives/java
lrwxrwxrwx 1 root root 43  6월  9 22:20 /etc/alternatives/java -> /usr/lib/jvm/java-12-openjdk-amd64/bin/java

readlink -f /usr/bin/java 명령으로도 동일한 결과를 볼 수 있습니다.

이 링크는 update-alternatives 로 관리됩니다. 아래와 같은 명령으로 현재 설치된 버전들과 우선 순위를 확인할 수 있습니다.

sudo update-alternatives --display java

수동으로 다운로드 압축을 풀어서 설치하거나 SDKMAN, Jabba등으로 설치한 JDK가 있다면 아래 명령으로 update-alternatives 의 관리대상에 추가할 수 있습니다.

sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/jdk1.8.0_31/bin/java 1000

심볼릭 링크로 연결되는 버전을 바꾸고 싶다면 아래와 같이 입력합니다.

sudo update-alternatives --config java

설치된 버전을 확인하고 번호를 선택해서 심볼릭 링크를 바꿀 수 있습니다.

There are 4 choices for the alternative java (providing /usr/bin/java).

  Selection    Path                                            Priority   Status
------------------------------------------------------------
* 0            /usr/lib/jvm/java-12-openjdk-amd64/bin/java      1211      auto mode
  1            /usr/lib/jvm/java-11-openjdk-amd64/bin/java      1111      manual mode
  2            /usr/lib/jvm/java-12-openjdk-amd64/bin/java      1211      manual mode
  3            /usr/lib/jvm/java-13-openjdk-amd64/bin/java      1211      manual mode
  4            /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java   1081      manual mode

Press <enter> to keep the current choice[*], or type selection number:

그런데 명령행에서 실행한 java 가 어느 곳으로 연결될지는 환경변수 PATH 에 영향을 받습니다. /usr/bin/java 보다 더 우선 순위가 높게 먼저 선언된 디렉토리에 java`가 있다면 `update-alternatives 에서 지정한 java가 실행되지 않을 수도 있습니다. SDKMAN, Jabba 등을 함께 사용한다면 이 점을 유의해야 합니다. 현재 쉘, 디렉토리에서 어느 java 를 실행하고 있는지는 which java 로 확인할 수 있습니다.

  • 장점

    • OS에서 기본적으로 제공하는 도구라서 별도의 설치 과정이 필요하지 않습니다.

    • YUM/APT 과 자연스럽게 함께 쓰이는 도구입니다.

  • 단점

    • 심블릭 링크로 쉘에서 사용할 디폴트 버전을 지정하는 기능만 있습니다.

2.3. Homebrew

macOS에서 많이 쓰는 범용 패키지 관리 프로그램입니다.

Homebrew로 AdoptOpen JDK배포판은 아래와 같이 설치할 수 있습니다.

brew tap AdoptOpenJDK/openjdk
brew cask install adoptopenjdk11

제가 macOS를 써본적이 없어서 Homebrew로 설치하는 방법에 대해서는 homebrew로 opendjk 설치하기 글을 참조했습니다.

2.4. Chocolatey

Chocolatey는 Windows OS를 위한 패키지 관리자입니다. Linux에는 APT/YUM, macOS에는 Homebrew가 있다면 Windows에는 Chocolatey가 대표적인 패키지 관리자입니다. https://chocolatey.org/install 을 참고해서 설치할수 있습니다.

Chocolatey로 설치가능한 JDK 패키지는 https://chocolatey.org/packages?q=jdk 으로 확인하실 수 있습니다.

:chocolatey-jdk.jpg

Oracle의 OpenJDK 빌드나 Adopt OpenJDK 배포판 등을 아래와 같이 설치할 수 있습니다.

Oracle의 OpenJDK 빌드 최신 버전 설치
choco install openjdk
AdoptOpenJDK 최신 버전 설치
choco install adoptopenjdk
Corretto 11 버전 설치
choco install corretto11jdk
zulu 최신 버전 설치
choco install zulu

--version 옵션을 붙이면

Oracle의 OpenJDK 빌드 11.0.2 버전 설치
choco install openjdk --version 11.0.2

위의 명령이 수행되고 나면 \Program Files\ 디렉토리 아래에 JDK 들이 위치하게 됩니다.

  • Oracle의 OpenJDK 빌드 : \Program Files\OpenJDK

  • Adopt OpenJDK : \Program Files\AdoptOpenJDK

  • Corretto : \Program Files\Coretto

  • Zulu : \Program Files\zulu

그런대 Chocolatey는 여러 JDK 버전을 동시에 쓰는 쓰임새가 우선적으로 고려되지는 않았습니다. JDK 12.0.1을 설치후에 11.0.2을 뒤에 설치하려고하면, 다운그레이드가 된다는 경고 메시지가 나옵니다. 이럴 때에는 '-sidebyside' 혹은 --force 등의 옵션을 붙여줘야합니다. JAVA_HOME 도 마지막으로 설치한 JDK의 위치로 지정됩니다. echo %java_home% 명령으로 이를 확인해 볼 수 있습니다. 여러 배포판을 설치할 경우 PATH 환경 변수의 값도 새로 설치한 배포판의 %JAVA_HOME%\bin 디렉토리가 뒤 쪽에 계속 추가만 됩니다.

  • 장점

    • JDK 외에도 Maven, Gradle의 설치에도 활용할 수 있는 범용적인 패키지 관리 도구입니다.

  • 단점

    • SDKMAN/ Jabba에 비하면 다양한 JDK 배포판을 제공하지는 않습니다.

    • 여러 버전을 동시에 설치할 수 있는 동작이 디폴트가 아닙니다.

      • OS 명령행에서 여러 JDK 버전을 함께 사용하려면 direnv등 별도의 프로그램과 함께 쓰는 것이 좋습니다.

2.5. direnv

direnv 는 특정 디렉토리와 그 하위 디렉토리에서만 사용할 환경 변수를 지정할 수 있는 도구입니다. Linux와 macOS에서 사용할 수 있습니다. 설치 방법은 https://direnv.net/ 을 참조합니다.

direnv에서 참조하는 .envrc 라는 파일에 PATH, JAVA_HOME 을 아래와 같이 지정할 수 있습니다.

export JAVA_HOME=/home/benelog/.sdkman/candidates/java/12.0.1.hs-adpt
export PATH=$JAVA_HOME/bin:$PATH

파일을 처음 생성하거나 변경했을 때에는 direnv allow . 명령을 한번 내려줘야합니다. 이 파일이 의도하지 않게 생성/수정 되었을 때 보안을 위한 장치입니다.

이후로 이 파일이 있는 디렉토리에 들어가면 이 환경변수가 활성화됩니다. cd 명령으로 디렉토리에 들어가면 아래와 같은 메시지가 콘솔에 보입니다.

direnv: loading .envrc
direnv: export ~JAVA_HOME ~PATH

보편적으로 사용할 수 있는 도구이기에 JAVA_HOME 외의 다른 환경 변수도 관리할 수 있습니다. 같은 프로젝트를 하더라도 개발자의 PC마다 달라지는 값이나 테스트를 위한 변수도 .envrc 에 넣어둘만합니다. 그럴 경우에는 .envrc.gitignore 에 추가해서 Git 저장소에는 들어가지 않도록 해야 하겠습니다.

  • 장점

    • JAVA_HOME 이나 PATH 외의 환경 변수도 관리할 수 있습니다.

  • 단점

    • 특정 디렉토리 내에서의 환경 변수 기능만 제공합니다.

2.6. jEnv

jEnv 는 JDK 버전관리만을 위한 전용 도구입니다.

아래와 같이 add 명령으로 관리할 버전을 추가합니다.

jenv add /usr/lib/jvm/java-11-openjdk-amd64/

add 로 지정한 디렉토리에서 JDK의 버전을 인식하여 아래와 같은 메시지가 나옵니다.

jenv add 명령의 결과
openjdk64-11.0.3 added
11.0.3 added
11.0 added

설치된 버전은 jenv versions 명령으로 확인할 수 있습니다.

jenv versions 명령의 결과
  system
  1.8
  1.8.0.212
* 11.0 (set by JENV_VERSION environment variable)
  11.0.3
  openjdk64-1.8.0.212
  openjdk64-11.0.3

디폴트로 사용할 버전은 global 명령으로 지정합니다.

jenv global 11.0

해당 쉘에서 임시로 사용할 버전은 shell 명령으로 지정합니다.

jenv shell 11.0

현재 디렉토리에서 사용할 버전은 local 명령으로 지정합니다.

jenv local 11.0

위와 같이 디렉토리에 지정된 버전은 .java-version 이라는 파일에 저장됩니다. 다음 번에 같은 디렉토리에서 java를 실행하면 이 파일에 지정된 해당 버전이 선택됩니다.

JAVA_HOME 환경 변수가 제대로 지정되기 위해서는 jENV의 export plugin을 아래 명령으로 활성화해줘야 합니다.

jenv enable-plugin export

jEnv를 다른 도구와 잘 어우러지게 사용하기 위해서는 동작 원리를 알아두는 것이 좋습니다. jEnv로 JDK 버전을 지정한 후 which java 로 어느 디렉토리에 있는 java 와 연결되는지 확인을 해보면 ~/.jenv/shims/java 가 나옵니다. 이 파일의 내용을 보면 실제 설치한 JDK의 java 가 아닌 쉘 스크립트라는 것을 알수 있습니다.

cat ~/.jenv/shims/java 명령의 결과
#!/usr/bin/env bash
set -e
[ -n "$JENV_DEBUG" ] && set -x

program="${0##*/}"
if [ "$program" = "java" ]; then
  for arg; do
    case "$arg" in
    -e* | -- ) break ;;
    */* )
      if [ -f "$arg" ]; then
        export JENV_DIR="${arg%/*}"
        break
      fi
      ;;
    esac
  done
fi

export JENV_ROOT="/root/.jenv"
exec "/root/.jenv/libexec/jenv" exec "$program" "$@"

따라서 다른 도구와 병행해서 사용할 경우, 환경변수 $PATH`에 `~/.jenv/shims/java`가 다른 도구에서 넣어준 JDK와 연결된 경로들보다 앞에 있어야 jEnv에서 설정한 버전대로 `java 가 실행됩니다.

$JAVA_HOME`도 어떻게 지정되어 있는지 `echo $JAVA_HOME 로 확인을 해보면 ~/.jenv/versions/11.0 와 같이 지정되어 있습니다. `~/.jenv/versions/ 디렉토리에 각 버전별로 실제로 JDK가 설처되어있는 디렉토리로의 심볼릭 링크가 들어가 있습니다.

~/.jenv/versions 디렉토리 안의 심볼릭 링크
lrwxrwxrwx  1 benelog benelog   33 Jun 30 17:05 1.8 -> /usr/lib/jvm/java-8-openjdk-amd64/
lrwxrwxrwx  1 benelog benelog   33 Jun 30 17:05 1.8.0.212 -> /usr/lib/jvm/java-8-openjdk-amd64/
lrwxrwxrwx  1 benelog benelog   34 Jun 30 17:08 11.0 -> /usr/lib/jvm/java-11-openjdk-amd64/
lrwxrwxrwx  1 benelog benelog   34 Jun 30 17:08 11.0.3 -> /usr/lib/jvm/java-11-openjdk-amd64/
lrwxrwxrwx  1 benelog benelog   33 Jun 30 17:05 openjdk64-1.8.0.212 -> /usr/lib/jvm/java-8-openjdk-amd64/
lrwxrwxrwx  1 benelog benelog   34 Jun 30 17:08 openjdk64-11.0.3 -> /usr/lib/jvm/java-11-openjdk-amd64/

그런데 jEnv는 여러 배포판을 동시에 설치할 때는 충돌을 일으킬수 있습니다. 예를 들어 Ubuntu 패키지 저장소의 OpenJDK 11을 이미 'jenv add' 로 넣은 다음, AdoptOpenJDK 11을 추가하면 아래와 같이 이미 존재하는 버전이라는 메시지가 나옵니다.

`jenv add /usr/lib/jvm/adoptopenjdk-11-jdk-hotspot 실행결과
 openjdk64-11.0.3 already present, skip installation
 11.0.3 already present, skip installation
 11.0 already present, skip installation

jEnv는 동일한 JDK 배포판의 여러 버전을 관리하는데 적합합니다.

  • 장점

    • 다양한 범위(디폴트(global), 디렉토리별, 쉘 범위)의 버전 방식을 지원합니다.

  • 단점

    • 다양한 배포판의 동일한 JDK 버전(예: 11.0.3)을 관리할 수 없습니다.

2.7. SDKMAN

SDKMAN(The Software Development Kit Manager)은 여러 개발도구를 설치할 수 있는 도구입니다. JDK 뿐만 아니라 Maven, Gradle, Ant, AsciidoctorJ 등 JVM 세계의 다양한 도구들을 설치할 수 있습니다.

OS별로 SDKMAN을 설치하는 방법은 https://sdkman.io/install 을 참조합니다.

SDKMAN으로 설치할 수 있는 JDK 배포판/버전은 sdk list java 명령으로 확인할 수 있습니다. 아래와 같이 사용할 수 있는 배포판들과 설치된 버전 등을 표시해 줍니다.

================================================================================
Available Java Versions
================================================================================
 Vendor        | Use | Version      | Dist    | Status     | Identifier
--------------------------------------------------------------------------------
 AdoptOpenJDK  |     | 12.0.1.j9    | adpt    |            | 12.0.1.j9-adpt
               |     | 12.0.1.hs    | adpt    | installed  | 12.0.1.hs-adpt
               |     | 11.0.3.j9    | adpt    |            | 11.0.3.j9-adpt
               |     | 11.0.3.hs    | adpt    |            | 11.0.3.hs-adpt
               |     | 8.0.212.j9   | adpt    |            | 8.0.212.j9-adpt
               | >>> | 8.0.212.hs   | adpt    | installed  | 8.0.212.hs-adpt
 Amazon        |     | 11.0.3       | amzn    |            | 11.0.3-amzn
               |     | 8.0.212      | amzn    |            | 8.0.212-amzn
 Azul Zulu     |     | 12.0.1       | zulu    |            | 12.0.1-zulu
               |     | 11.0.3       | zulu    |            | 11.0.3-zulu
               |     | 10.0.2       | zulu    |            | 10.0.2-zulu
               |     | 9.0.7        | zulu    |            | 9.0.7-zulu
               |     | 8.0.212      | zulu    |            | 8.0.212-zulu
               |     | 7.0.222      | zulu    |            | 7.0.222-zulu
               |     | 6.0.119      | zulu    |            | 6.0.119-zulu
 Azul ZuluFX   |     | 11.0.2       | zulufx  |            | 11.0.2-zulufx
               |     | 8.0.202      | zulufx  |            | 8.0.202-zulufx
 BellSoft      |     | 12.0.1       | librca  |            | 12.0.1-librca
               |     | 11.0.3       | librca  |            | 11.0.3-librca
               |     | 8.0.212      | librca  |            | 8.0.212-librca
 GraalVM       |     | 19.0.2       | grl     |            | 19.0.2-grl
               |     | 19.0.0       | grl     |            | 19.0.0-grl
               |     | 1.0.0        | grl     | installed  | 1.0.0-rc-16-grl
 SAP           |     | 12.0.1       | sapmchn |            | 12.0.1-sapmchn
               |     | 11.0.3       | sapmchn |            | 11.0.3-sapmchn
 java.net      |     | 14.ea.1      | open    |            | 14.ea.1-open
               |     | 13.ea.25     | open    |            | 13.ea.25-open
               |     | 12.0.1       | open    |            | 12.0.1-open
               |     | 11.0.2       | open    |            | 11.0.2-open
               |     | 10.0.2       | open    |            | 10.0.2-open
               |     | 9.0.4        | open    |            | 9.0.4-open
================================================================================
Use the Identifier for installation:

    $ sdk install java 11.0.3.hs-adpt
================================================================================

AdoptOpenJDK HotSpot 배포판 12.0.1 버전을 설치하고 싶다면 아래와 같은 명령을 내립니다.

sdk install java 12.0.1.hs-adpt

PATH , JAVA_HOME 환경변수도 알아서 잘 잡아줍니다.

명령행에서 디폴트로 사용할 JDK 버전은 ~/.sdkman/candidates/java/current 에서 심볼릭 링크로 관리됩니다. 이 링크가 환경변수 $PATH`와 `$JAVA_HOME 에 추가 됩니다.

이 심볼릭 링크는 아래 명령으로 바꿀 수 있습니다.

sdk default java 8.0.212.hs-adpt

현재 쉘에서 사용할 버전만 임시로 바꾸고 싶다면 default 대신 use 명령을 씁니다.

sdk use java 8.0.212.hs-adpt
  • 장점

    • 다양한 JDK 배포판을 설치할 수 있습니다.

    • JDK 설치와 버전 지정을 하나의 도구로 관리할 수 있습니다.

  • 단점

    • 특정 디렉토리에 들어갔을 때 사용할 버전을 자동을 지정하는 기능이 없습니다.

    • sdk use 명령이 jabba의 동일한 기능에 비해 실행 속도가 느립니다.

2.8. jabba

jabba는 JDK의 설치/버전 관리만을 위한 도구입니다.

각 OS별 jabba의 설치 방법은 https://github.com/shyiko/jabba#installation 을 참조합니다.

설치할 수 있는 JDK의 배포판은 jabba ls-remote 명령으로 확인할 수 있습니다. 이중 Amazon에서 제공하는 Corretto 배포판 JDK 11을 설치한다면 아래와 같은 명령을 내립니다.

jabba install amazon-corretto@1.11.0-3.7.1

설치된 버전들은 jabba ls 명령으로 확인할 수 있습니다. 현재 쉘에서 사용할 버전은 아래와 같이 지정할 수 있습니다.

jabba use adopt-openj9@1.12.33-0

jabba use 를 실행하면 PATHJAVA_HOME 환경변수를 지정한 JDK 버전을 참조할수 있도록 바꾸어줍니다. echo $PATH 로 PATH 값을 확인해보면, 가장 앞에 설치한 JDK의 bin 디렉토리를 지정할 것을 확인할 수 있습니다.

같은 디렉토리에 `.jabbarc`라는 파일이 있다면, 그 파일에 지정된 버전을 참조할 수 있습니다. 즉 아래와 같이 실행해도 특정 버전을 지정할 수 있습니다.

echo "adopt-openj9@1.12.33-0" > .jabbarc
jabba use

다음 번에 같은 디렉토리에 들어왔을 떄에는 jabba use 만 간단하게 실행해서 같은 효과를 낼 수 있습니다. direnv나 jEnv를 쓸 때처럼 디렉토리에 들어가면 자동으로 환경변수를 바꾸어주는 기능은 없습니다.

현재 쉘범위의 JDK 버전만 지정한다는 점이 jabba의 장점이나 단점입니다.

  • 장점

    • 다른 도구와 충돌없이 쓰기에 좋습니다.

    • jabba use 명령이 SDKMAN의 sdk use 에 비해 실행 속도가 빠릅니다.

  • 단점

    • 디폴트 버전 지정이 없습니다.

    • 디렉토리별 버전 비전 기능이 완전 자동이 아닙니다. 해당 디렉토리에서 jabba use 를 한번 입력해야 합니다.

3. 무엇을 어떻게 사용할 것인가?

위의 다양한 도구 중 어떤 것을 골라 쓸지는 개발장비의 OS와 필요한 범위에 따라서 결정해야할 것입니다.

우선 다양한 배포판의 JDK를 쓰는 것까지 필요가 없다면 아래 정도의 조합을 고려할만합니다.

  • Windows : Chocolatey + direnv

  • Linux : APT/YUM + update-alternatives + jEnv (또는 direnv)

  • macOS : Homebrew + jEnv(또는 direnv)

    • [Mac에 Java 여러 버전 설치] 글에서는 Homebrew로 Oracle JDK를 설치하고 jEnv와 함께 사용하는 사례가 정리되어 있습니다.

Amazon Corretto, GraalVM 등 다양한 배포판의 여러버전을 설치해보고 싶다면 SDKMAN이나 jabba를 함꼐 쓰는 것을 추천합니다. 각 도구들이 지원하는 배포판은 아래와 같습니다. (2019년 7월1일 기준)

Table 2. JDK 설치 도구들이 지원하는 배포판
이름 지원하는 JDK 배포판

YUM/APT

OS 배포판별 OpenJDK (*1)
AdoptOpen JDK

Homebrew

Oracle JDK
Adopt OpenJDK

Chocolatey

Oracle JDK
Oracle의 OpenJDK 빌드 (*2)
Adopt OpenJDK
Amazon Corretto
Zulu OpenJDK

SDKMAN

Oracle의 OpenJDK 빌드 (*2)
Adopt OpenJDK
Amazon Corretto GraalVM CE
Zulu OpenJDK
Zulu OpenJDK + OpenJFX
SapMachine
Liberica JDK

jabba

Oracle JDK
Oracle의 OpenJDK 빌드 (*2)
Adopt OpenJDK
Amazon Corretto
GraalVM CE
Zulu OpenJDK
IBM SDK
OpenJDK 참조 구현체
OpenJDK + Shenandoah GC
Liberica JDK

  • (*1) : 해당 OS 배포판을 위해 빌드된 OpenJDK 배포판입니다. OS의 배포판을 관리하는 업체/커뮤니티에서 관리합니다.

  • (*2) : https://jdk.java.net/ 에서 다운로드 받을 수 있는 OpenJDK 배포판입니다. 출시 후 6개월까지만 최신 버전이 업데이트됩니다.

위에 정리한 것처럼 SDKMAN과 jabba가 많은 JDK 배포판을 지원합니다. 둘다 Adopt OpenJDK, Amazon Corretto, GraalVM CE, Zulu 등 주목받는 주요 배포판은 모두 포함하고 있습니다.

SDKMAN에서는 제공하는 반면 jabba에는 없는 배포판은 아래와 같습니다.

  • Zulu OpenJDK + OpenJFX

  • SapMachine

jabba에서는 제공하는 반면 SDKMAN에는 없는 배포판은 아래와 같습니다.

SDKMAN과 jabba는 JDK 설치와 버전 지정 기능을 동시에 제공합니다. 그런데 jenv등 다른 도구에서 제공하는 버전 지정 기능을 완정히 제공하지는 않습니다.

Table 3. JDK 버전 지정 기능
도구 디폴트 디렉토리별 쉘 범위

update-alternatives/ alternatives

O

X

X

SDKMAN

O

X

O

jabba

X

(*3)

O

jEnv

O

O

O

direnv

X

O

X

  • (*3) : jEnv나 direnv처럼 디렉토리에 들어가면 자동으로 특정 JDK 버전이 선택되는 방식은 아니기 때문에 △로 표기했습니다.

따라서 SDKMAN이나 jabba는 다른 도구와 조합해서 사용하면 더욱 편리하게 쓸 수 있습니다. 그런데 앞서 언급했듯이 jEnv는 SDKMAN이나 jabba와 함께 쓰기에는 적합하지 않습니다. $PATH 환경 변수에 지정된 경로의 순서에 따라서 여러 도구의 버전 지정 결과가 의도하지 않게 덮어 써질수 있습니다. 즉 SDKMAN에 지정한 경로가 앞에 있으면 jEnv에서 지정한 JDK 버전이 인식되지 않는 것처럼 보일수도 있습니다. 그리고 jabba로는 여러 배포판의 JDK 11.0.3 을 설치할 수 있지만 jEnv에서는 'jenv add' 로 같은 버전(11.0.3)의 다른 배포판을 추가할 수 없습니다.

따라서 다양한 배포판을 설치하고자 할때는 SDKMAN(또는 jabba) + direnv 조합을 추천합니다.

제가 이 도구들을 쓰는 환경은 아래와 같습니다.

  • 각각 다른 JDK 버전을 쓰는 여러 프로젝트의 소스를 고칩니다.

  • 업무 혹은 취미로 JDK의 여러 배포판/ 버전을 설치해서 차이가 있는지 확인하고 있습니다.

    • (예: 포함된 ca-cert 목록 비교, GraalVM으로 네이티브 이미지 만들기 시도)

  • 회사의 업무용 노트북과 집에 있는 PC에서 Ubuntu 19.04를 씁니다.

이에 따라 저는 아래와 같이 도구를 조합해서 쓰고 있습니다.

  • JDK 설치에는 APT, SDKMAN, jabba를 다 사용해 보고 있습니다.

  • 사용할 버전을 선택할 때는

    • 디폴트 버전은 SDKMAN으로 지정합니다.

      • SDKMAN을 설치하면 SDK에서 관리하는 패키지들이 /usr/bin 보다 앞에 오기 때문입니다. 디폴트 버전은 자주 바꾸진 않기 때문에 굳이 이를 조정하진 않았습니다.

    • 특정 디렉토리에서 사용한 버전을 지정할 때는 direnv를 씁니다.

    • 쉘에서 일시적으로 사용할 버전을 지정할 때는 SDKMAN, jabba를 씁니다.