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
문(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