JDK 17 LTS 정리

JDK 17 LTS 정리

자바 12 ~ 17사이의 주요 변경사항


JDK 17 LTS 정리


올해 4분기에 Spring Framework 6, Spring Boot 3이 릴리즈될 예정이며, 현 시점 두 버전 모두 milestone 버전이 이미 출시돼있는 상태이다.

명심해야 할 것은 두 버전 모두 최소 JDK 17을 사용하는것이 필수 조건이라는 것이다.

이번에 Spring Framework 프로젝트에 기여하기 위해 프로젝트 환경설정을 진행했는데, 위 사실에 근거해 JDK 17도 함께 설치해야 했었다.

이러한 상황에 JDK 17을 슬슬 공부해야 할 시기가 다가옴을 직접적으로 체감하였고, JDK 17을 설치하고 2일간 사용해보며 12 ~ 17 까지의 변경사항중 눈에 띄는 부분들을 정리해보았다.

특이사항으로는 JDK 11 + Spring Boot 2 조합에서 JDK 17로 업그레이드하면 하위호환성으로 인한 에러가 많이 발생할 줄 알았는데, 의외로 하위호환성이 매우 훌륭하다고 느꼈다.

필자는 Spring Boot 2.6.3에 JDK 17을 사용했을 경우 별다른 문제없이 잘 돌아가는 것을 확인했는데, 조금 더 크고 복잡한 시스템이라면 어땠을지 또 모르겠다.


주요 변경점

NullPointerExceptions


가히 혁명적인 피처가 아닐까 싶다.

기존 자바에서 NullPointerException이 발생하면 대략 다음과 같은 메시지가 출력됐었다.


Exception in thread "main" java.lang.NullPointerException


그래서 어디서 NullPointerException이 발생했는지 직접 추적해야 했으며, 이 작업이 매우 고통스럽기 때문에 이를 빨리 알아차리기 위해 Objects.requireNonNull()과 같은 메서드가 사용되곤 했다. (fast-fail)

모던자바에서는 어디서 NullPointerException이 발생했는지 직접 알려준다.

NullPointerException이 발생하도록 작성한 다음과 같은 코드가 있다.


@Test
void nullPointerException() throws Exception {
    String message = null;
    printToUpperCase(message);
}

private void printToUpperCase(String message) {
    System.out.println("message = " + message.toUpperCase());
}


이를 실행하면 모던자바에서는 이제 다음과 같은 메시지가 출력된다.


Cannot invoke "String.toUpperCase()" because "message" is null
java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because "message" is null
	at io.github.shirohoo.JDK17Tests.nullPointerException(JDK17Tests.java:52)


Switch Expression


📜 스위치 JEP 명세


문(Statement)식(Expression)의 차이는 반환값이 있느냐 없느냐인데, JDK 11 LTS까지의 스위치는 문이었다. (스위치문 !)

기존 자바의 스위치문은 다음과 같다.


switch (caseType) {
case 1:
	break;
case 2:
	break;
case 3:
	break;
default:
	break;
}


마틴 파울러의 Refactoring 책에서는 소프트웨어 설계적인 관점에서 볼 때 이러한 코드를 나쁜냄새가 풍긴다고 표현하였고, 상속구조를 통한 리팩토링을 권장하였다.

하지만 모던자바에서 변경된 스위치식은 좀 다르다.

우선 스위치식 자체가 반환값을 가질 수 있는게 가장 큰 특징이며, 코드의 가독성도 좋아졌다.

스위치가 반환값을 갖지 않을 경우의 코드는 다음과 같다.

->를 통해 람다식처럼 표현이 가능해졌다.


private void switchEnum(DayOfWeek day) {
        switch (day) {
            case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
            case TUESDAY -> System.out.println(7);
            case THURSDAY, SATURDAY -> System.out.println(8);
            case WEDNESDAY -> System.out.println(9);
        }
    }


스위치가 반환값을 갖기 위해서는 default 구문이 반드시 추가되어야만 한다.

아무런 케이스에도 해당하지 않을 경우 반환되어야 할 값이 정의돼야하기 때문이다.


private String switchExpression(int number) {
        return switch (number) {
            case 1, 2 -> "one or two";
            case 3, 4 -> "three or four";
            case 5, 6 -> "five or six";
            default -> "unknown value";
        };
    }


만약 스위치식에서 한줄의 코드가 아닌 여러줄의 코드를 작성해야 한다면 yield 예약어를 사용할 수 있다. (return과 같은역할을 한다)

단 yield는 반드시 블록({}, 중괄호)안에서 사용되어야 하므로, 스위치 케이스가 한줄로 정의될 경우에는 사용할 수 없으며, 사용하면 컴파일 에러가 발생한다.

기존의 콜론(:)을 사용하던 스위치문은 그 자체로 컴파일러에서 블록으로 인식하기 때문에 yield를 사용할 수 있다.


private String switchExpression(int number) {
        return switch (number) {
            case 1, 2 -> "one or two";
            case 3, 4 -> "three or four";
            case 5, 6 -> "five or six";
            default -> {
                System.out.println("No matching cases");
                yield "unknown value";
            }
        };
    }


Text Block


매우 간단하다. 여타 모던 언어에서 지원하는 텍스트 블록과 같다.

가장 마음에 드는 기능중 하나였다.

주의할 점은 텍스트 블록은 """ ~ """ 사이에 있는 모든 엔터값을 개행 문자로 인식한다. (첫줄 “”” 다음에 오는 첫번째 개행은 제외하며, 마지막 “"”앞에있는 개행문자는 인식된다)


@Test
void stringBlock() throws Exception {
    // 기존 자바의 방식
    String beforeString = "{\n" +
        "  \"name\": \"shirohoo\",\n" +
        "  \"age\": 30,\n" +
        "}";

    // 모던자바의 텍스트블록으로 표현할 경우 1
    String afterString = """
        {
          "name": "shirohoo",
          "age": 30,
        }"""; // 마지막 """ 앞에 개행문자가 없음을 주의깊게 볼 것!
	
    // 모던자바의 텍스트블록으로 표현할 경우 2
    String afterString = """
        {
          "name": "shirohoo",
          "age": 30,
        }
	""".stripTrailing(); // 마지막 """ 앞에 개행문자를 넣었을 경우 이와 같이 처리할수도 있다
}


그냥 뭐… 압도적이다

다만, 공백관련 문제가 약간 있어 이를 보완하기 위한 몇가지 메서드들이 추가됐다.

더 자세한 사항은 📜 아주 좋은 아티클이 있어 이를 첨부한다.

  • String.stripIndent(): 텍스트블록에서 생성되는 부수적인 공백들을 제거하는데 사용한다
  • String.translateEscapes(): escape sequences를 번역하는 데 사용된다. (\b, \f, \n, \t, \r, ", ', \ and octal escapes)
  • String.formatted(Object… args): 텍스트 블록에 사용한 Placeholder를 치환한다. (%s, %d와 같은 것들)
String output = """
    Name: %s
    Phone: %s
    Address: %s
    Salary: $%.2f
    """.formatted(name, phone, address, salary);


Record Class


새로 추가된 클래스인데, 자바에서 새로운 타입의 클래스가 추가된 것은 JDK 5에서 추가된 enum 이후 최초라고 한다.

코틀린(Kotlin)data class와 같은 기능을 하며, 간단하게 말하자면 읽기전용 불변 최종 클래스가 된다.

클래스를 record로 선언하면 해당 클래스는 final 클래스가 되어 abstract로 선언할 수 없게되며, 모든 클래스 멤버도 final이 된다.

또한, 모든 클래스멤버에 대한 생성자public 제한자를 갖는 getter, equals, hashCode, toString 메서드가 자동으로 생성된다.


public record Member(String name, int age) {}


위 코드(모던 자바 방식)는 하기 코드(기존 자바 방식)와 정확히 동일하다.


public final class Member {
    private final String name;
    private final int age;

    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String name() {
        return name;
    }

    public int age() {
        return age;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this)
            return true;
        if (obj == null || obj.getClass() != this.getClass())
            return false;
        var that = (Member) obj;
        return Objects.equals(this.name, that.name) &&
            this.age == that.age;
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public String toString() {
        return "Member[" +
            "name=" + name + ", " +
            "age=" + age + ']';
    }
}


Pattern matching for instanceof


팩토리 클래스에서 특히 유용할 것 같은 기능이여서 추가한다.

기존 자바에서는 instanceof 키워드를 통해 타입체크를 한 후 타입 캐스팅을 해야만 했다.


Object o = "";
if(o instanceof String) {
    String message = (String) o;
    System.out.println("message = " + message);
}


위 코드는 이제 다음과 같이 타입 캐스팅과 변수 선언 및 할당을 처리 할 수 있다.


Object o = "";
if(o instanceof String message) {
    System.out.println("message = " + message);
}


Number Formatting Support


NumberFormat에 포매팅을 도와주는 정적팩토리 메서드가 생겼다.

다음과 같이 아무런 인수를 주지 않을 경우 사용하고 있는 머신의 Locale을 따라간다.


NumberFormat fmt = NumberFormat.getCompactNumberInstance();
System.out.println(fmt.format(1000));
System.out.println(fmt.format(100000));
System.out.println(fmt.format(1000000));


1천
10만
100만


혹은 별도로 Locale을 할당할수도 있다.

NumberFormat fmt = NumberFormat.getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.SHORT);


1K
100K
1M


Stream.toList()


기존 자바에서는 Stream에서 List를 반환하도록 하기 위해 다음과 같이 장황한 코드를 작성해야만 했다.


List<String> strings = Stream.of("a", "b", "c")
            .collect(Collectors.toList());


모던 자바에서는 이를 다음과 같이 표현할 수 있다.


List<String> strings = Stream.of("a", "b", "c").toList();


ZGC


모던 자바에서 가장 큰 변화라고 하는데, JVM 최적화가 어마어마하게 이루어진 듯 하다.

하지만 필자는 아직 이러한 로우레벨에 대해 완벽하게 이해하고 있지 못하고, GC 튜닝을 해본 경험이 없기 때문에 관련 포스팅을 첨부한다.


📜 JVM과 Garbage Collection - G1GC vs ZGC


중요하다고 생각되는 부분은 ZGC가 64bit 컴퓨터에서만 지원되며, G1GC 대비 성능이 매우 좋다는 부분인 듯 하다.


Sealed Class


📜 JEP 360: Sealed Classes (Preview)


일견 보기에 상속받을 수 있는 클래스를 제한한다는 내용으로 보이는데, 아직 무슨 내용인지 제대로 이해하지 못한것 같다.

어디에 사용해야 할 지 지금으로서는 잘 모르겠다.

우선 이런것도 있구나 하고 넘어갔다.


12 ~ 17 Features

12 ~ 17의 전체적인 피처는 하기와 같다.

JDK 12

  • 189: Shenandoah: A Low-Pause-Time Garbage Collector (Experimental)
  • 230: Microbenchmark Suite
  • 325: Switch Expressions (Preview)
  • 334: JVM Constants API
  • 340: One AArch64 Port, Not Two
  • 341: Default CDS Archives
  • 344: Abortable Mixed Collections for G1
  • 346: Promptly Return Unused Committed Memory from G1

JDK 13

  • 350: Dynamic CDS Archives
  • 351: ZGC: Uncommit Unused Memory
  • 353: Reimplement the Legacy Socket API
  • 354: Switch Expressions (Preview)
  • 355: Text Blocks (Preview)

JDK 14

  • 305: Pattern Matching for instanceof (Preview)
  • 343: Packaging Tool (Incubator)
  • 345: NUMA-Aware Memory Allocation for G1
  • 349: JFR Event Streaming
  • 352: Non-Volatile Mapped Byte Buffers
  • 358: Helpful NullPointerExceptions
  • 359: Records (Preview)
  • 361: Switch Expressions (Standard)
  • 362: Deprecate the Solaris and SPARC Ports
  • 363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector
  • 364: ZGC on macOS
  • 365: ZGC on Windows
  • 366: Deprecate the ParallelScavenge + SerialOld GC Combination
  • 367: Remove the Pack200 Tools and API
  • 368: Text Blocks (Second Preview)
  • 370: Foreign-Memory Access API (Incubator)

JDK 15

  • 339: Edwards-Curve Digital Signature Algorithm (EdDSA)
  • 360: Sealed Classes (Preview)
  • 371: Hidden Classes
  • 372: Remove the Nashorn JavaScript Engine
  • 373: Reimplement the Legacy DatagramSocket API
  • 374: Disable and Deprecate Biased Locking
  • 375: Pattern Matching for instanceof (Second Preview)
  • 377: ZGC: A Scalable Low-Latency Garbage Collector
  • 378: Text Blocks
  • 379: Shenandoah: A Low-Pause-Time Garbage Collector
  • 381: Remove the Solaris and SPARC Ports
  • 383: Foreign-Memory Access API (Second Incubator)
  • 384: Records (Second Preview)
  • 385: Deprecate RMI Activation for Removal

JDK 16

  • 338: Vector API (Incubator)
  • 347: Enable C++14 Language Features
  • 357: Migrate from Mercurial to Git
  • 369: Migrate to GitHub
  • 376: ZGC: Concurrent Thread-Stack Processing
  • 380: Unix-Domain Socket Channels
  • 386: Alpine Linux Port
  • 387: Elastic Metaspace
  • 388: Windows/AArch64 Port
  • 389: Foreign Linker API (Incubator)
  • 390: Warnings for Value-Based Classes
  • 392: Packaging Tool
  • 393: Foreign-Memory Access API (Third Incubator)
  • 394: Pattern Matching for instanceof
  • 395: Records
  • 396: Strongly Encapsulate JDK Internals by Default
  • 397: Sealed Classes (Second Preview)

JDK 17

  • 306: Restore Always-Strict Floating-Point Semantics
  • 356: Enhanced Pseudo-Random Number Generators
  • 382: New macOS Rendering Pipeline
  • 391: macOS/AArch64 Port
  • 398: Deprecate the Applet API for Removal
  • 403: Strongly Encapsulate JDK Internals
  • 406: Pattern Matching for switch (Preview)
  • 407: Remove RMI Activation
  • 409: Sealed Classes
  • 410: Remove the Experimental AOT and JIT Compiler
  • 411: Deprecate the Security Manager for Removal
  • 412: Foreign Function & Memory API (Incubator)
  • 414: Vector API (Second Incubator)
  • 415: Context-Specific Deserialization Filters



© 2022. All rights reserved.