
JDK 21 LTS Release
2023년 9월 19일, 자바 17을 이을 차기 LTS 버전인 Open JDK 21가 General Availability로 릴리즈되었다.
General Availability: Final release, ready for production use
JDK 21은 15개의 신규 기능으로 구성되며, 크게 4개의 카테고리로 분류해보면 아래와 같다.
Core Java Library
JEP 431: Sequenced Collections
JEP 442: Foreign Function & Memory API (Third Preview)
JEP 444: Virtual Threads
JEP 446: Scoped Values (Preview)
JEP 448: Vector API (Sixth Incubator)
JEP 453: Structured Concurrency (Preview)
Java Language Specification
JEP 430: String Templates (Preview)
JEP 440: Record Patterns
JEP 441: Pattern Matching for switch
JEP 443: Unnamed Patterns and Variables (Preview)
JEP 445: Unnamed Classes and Instance Main Methods (Preview)
HotSpot
JEP 439: Generational ZGC
JEP 449: Deprecate the Windows 32-bit x86 Port for Removal
JEP 451: Prepare to Disallow the Dynamic Loading of Agents
Security Library
JEP 452: Key Encapsulation Mechanism API
이 중, JDK 17과 비교하여 아래의 주요 기능 위주로 정리한다.
- Sequenced Collections
- Virtual Threads
- Record Patterns
- Pattern Matching for switch
- Generational ZGC
- Prepare to Disallow the Dynamic Loading of Agents
- Key Encapsulation Mechanism API
JDK 21의 새로운 기능들
[ JEP 431: Sequenced Collections ]
SequencedCollection Interface가 새로 정의됨 : 특정한 순서로 연쇄적인 요소들을 가지는 Collection
✔ History
기존의 자바 컬렉션 프레임워크는 정해진 순서의 원소에 접근하는 것이 어려웠다.
일련의 순서를 가진다는 동일한 특성의 collection 임에도 불구, 다음과 같이 일관되지 못한 방법으로 원소에 접근하게 되었다.
| First element | Last element | |
| List | list.get(0) | list.get(list.size()-1) |
| Deque | deque.getFirst() | deque.getLast() |
| SortedSet | sortedSet.first() | sortedSet.last() |
| LinkedHashSet | linkedHashSet.iterator().next() | //missing |
이렇게, 특정 순서를 가진 특별한 Collection들을 위한 API가 필요하지만,
기존의 Collection은 너무 범용적이고 List는 너무 구체화된 클래스라 변경하기엔 알맞지 않았다.
이런 이유로, Sequenced Collection 이 도입되었다.
이를 통해 정해진 순서의 원소에 접근하고, 이를 역순으로 처리하는 일관된 API를 제공한다.

✔ SequencedCollection
SequencedCollection은 처음과 마지막 요소에 대한 공통된 기능을 제공한다.
reversed()는 새롭게 추가된 메소드이지만, 나머지는 Deque로부터 승격된 것이다.
interface SequencedCollection<E> extends Collection<E> {
// new method
SequencedCollection<E> reversed();
// methods promoted from Deque
void addFirst(E);
void addLast(E);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
}
Sequenced Collection 는 첫 번째와 마지막 요소를 통해 접근하며, Collection 연산을 사용할 때 처음부터 끝의 순서로 차례대로 처리하거나 forward, 반대 순서 reverse로 처리할 수 있다.
중간 요소들은 바로 이 전의 요소 predecessors 와 바로 다음 요소 successors를 가리키고 있다.
public interface List<E> extends SequencedCollection<E> {
// ...
default List<E> reversed() {
return ReverseOrderListView.of(this, true); // we must assume it's modifiable
}
}
새로운 reversed() 메소드는 원래 순서의 반대 순서로 보이도록 한다. ReverseOrderListView.of()의 두 번째 인자는 새로 생성할 리스트의 변경 가능 여부를 나타내는데, 기본이 true (modifiable=true) 이다.
✔ SequencedSet / SequencedMap
interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
SequencedSet<E> reversed(); // covariant override
}
SortedSet과 같은 컬렉션은 SequencedCollection 부모 인터페이스에 선언된 addFirst(E) 및 addLast(E) 메소드 등을 지원할 수 없기 때문에 UnsupportedOperationException을 던질 수 있다.
addFirst()와 addLast()는 LinkedHashSet과 같은 구현체의 요소를 재배치할 수 없다는 오랜 결함을 해소한다.
interface SequencedMap<K,V> extends Map<K,V> {
// new methods
SequencedMap<K,V> reversed();
SequencedSet<K> sequencedKeySet();
SequencedCollection<V> sequencedValues();
SequencedSet<Entry<K,V>> sequencedEntrySet();
V putFirst(K, V);
V putLast(K, V);
// methods promoted from NavigableMap
Entry<K, V> firstEntry();
Entry<K, V> lastEntry();
Entry<K, V> pollFirstEntry();
Entry<K, V> pollLastEntry();
}
SequenceSet에서와 마찬가지로 putFirst(K, V)와 putLast(K, V) 메소드는 원소가 이미 존재하는 경우에 적절한 위치로 재배치되는 특별한 의미를 갖는다.
이를 처리할 수 없는 SortedMap은 마찬가지로 UnsupportedOperationException을 던질 수 있다.
✔ Collections
새로운 인터페이스가 추가됨에 따라 Collections 유틸에도 다음과 같은 기능이 추가될 예정
Collections.unmodifiableSequencedCollection(sequencedCollection)
Collections.unmodifiableSequencedSet(sequencedSet)
Collections.unmodifiableSequencedMap(sequencedMap)
△ Caution
JDK 21 이전에 List를 확장한 클래스에 getFirst() 라는 이름의 메소드를 정의해서 사용하고 있을 경우,
JDK 21에서는 overload method가 되어 컴파일 오류가 발생하게 된다.
[ JEP 444: Virtual Threads ]
JAVA는 Multi-thread 기반으로 동시성 처리를 하였고, 이로 인해 I/O 요청이 들어오면 Thread가 블로킹되면서 자원이 낭비되곤 하였다. 이번 21버전에서는 Thread를 경량화하는 Virtual-thread를 공식적으로 도입하여 처리량이 많은 동시성 애플리케이션을 개발, 유지 및 관리하는데 드는 비용을 획기적으로 줄일 수 있게 되었다.
✔ Problems: Platform Thread
JDK는 기본적으로 Platform Thread(Java Thread)와 OS Thread가 일대일 관계로 동작한다.

따라서 Thread가 IO 연산을 완료할 때까지, OS Thread는 사용되지 않는 상태인 Block 상태로 대기한다.
이에 Application이 OS Thread의 사용 가능 여부에 의존해서 한계를 갖기 때문에,
Java 생태계의 확장성 측면에서 큰 문제가 되어왔다.
- 기존 자바의 스레드는 OS 스레드와 일대일 대응되도록 구현됨
- 이로 인해 스레드의 개수가 제한되며, I/O 작업 시에 블로킹되어 하드웨어를 최적으로 활용하지 못함
- Expensive Creation of Threads
- Java Thread는 메모리를 할당하고, 스택을 초기화한 후, OS Thread를 등록하기 위해 OS 호출을 하는 등 많은 무거운 작업이 필요하여 새로 생성하고 유지하는 비용이 비싸다.
- 따라서 Thread를 미리 생성하고 Thread Pool에 저장하는데, 애플리케이션의 안정성 때문에 Thread Pool이 제한되어야 한다.
- Expensive Context Switching
- OS Thread가 하나의 Java Thread에서 다른 Java Thread로 전환하려면 많은 CPU 사이클을 수반해야 하여 큰 비용이 든다.
- Expensive Creation of Threads
- 여러가지 대안에 대한 검토
- Async Programming
- 비동기 프로그래밍이 Thread 문제를 극복하기 위한 방식이 될 수 있지만, 작성하는 과정이 상당히 복잡하고 디버깅이 어렵다.
- 다른 언어들은 제약으로 인해 비동기 API, 코루틴 등으로 문제를 해결했지만 자바는 제약이 없음
- Async Programming
✔ Virtual Threads
가상 스레드는 OS가 아닌 JDK에서 제공하는 경량화된 user-mode 스레드이다. 특정 OS 스레드에 연결되지 않으므로 OS에서는 존재를 인식하지 못한다. CPU에서 연산을 수행하는 동안에만 OS 스레드를 사용하며, 대기 상태 동안 OS 스레드를 붙잡고 있지 않는다.
가상 스레드는 생성 비용이 저렴하고 거의 무한대로 생성 가능한 스레드로, 대부분은 수명이 짧고 호출 스택이 얕아 하드웨어 활용도가 최적에 가까워진다. 이에 높은 수준의 동시성을 구현, 결과적으로 높은 처리량을 제공하며, 기존 자바 플랫폼 및 도구들과 조화를 이룬다.

- Cheap Context Switching
- 가상 스레드는 JVM의 통제 하에 있기 때문에 Thread stack이 Stack에 저장되지 않고 Heap 메모리에 저장된다. 이는 활성 중인 가상 스레드에 Thread stack을 할당하는 것이 훨씬 저렴해지는 것을 뜻함
✔ Motivation
핵심 목표는 다음과 같다.
- thread-per-request 스타일의 서버 애플리케이션이 하드웨어를 최적으로 활용할 수 있도록 한다.
- java.lang.Thread API 수정을 최소화하면서 가상 스레드를 채택할 수 있도록 한다.
- 기존의 JDK 도구를 사용해 가상 스레드의 troubleshooting, debugging, profiling을 지원한다.
✔ How to Use
새로운 스레드를 생성하는 신규 API : Thread.Builder, Thread.ofVirtual(), Thread.ofPlatform()
Thread.Builder를 사용하여 동일한 속성의 여러 스레드를 가지는 ThreadFactory도 생성 가능하다.
Thread thread = Thread.ofPlatform()
.name("newThread")
.unstarted(runnable);
Thread thread = Thread.ofVirtual()
.name("newThread")
.unstarted(runnable);
Thread.startVirtualThread(Runnable);
// 또는
Thread.ofVirtual().start(() -> {
//
});
// 현재 스레드가 가상 스레드인지 검사
boolean isVirtual = Thread.isVirtual();
△ Caution
가상 스레드는 비용이 저렴한 스레드이므로 풀링하지 말아야 함
수백만 개의 가상 스레드가 리소스를 공유할 수 있으므로 ThreadLocal은 주의해서 사용
[ JEP 440: Record Patterns ]
이전 JDK 16에서는 instanceof 연산자가 타입을 비교해서, 특정 객체의 내부 값을 매칭시켜 가져와 패턴 매칭을 개선했다. (Type Patterns)
JDK 21부터는 레코드 타입에 대해 보다 개선된 방법(Record Patterns)으로 사용 가능해진다.
✔ Record Patterns
// As of Java 16
record Point(int x, int y) {}
static void printSum(Object obj) {
if (obj instanceof Point p) {
int x = p.x();
int y = p.y();
System.out.println(x+y);
}
}
// As of Java 21
record Point(int x, int y) {}
static void printSum(Object obj) {
if (obj instanceof Point(int x, int y)) {
System.out.println(x+y);
}
}
obj가 Point의 인스턴스인지 여부를 테스트할 뿐만 아니라 개발자를 대신해 접근자 메소드를 호출하여 직접 변수들에 접근할 수 있게 된다. Record Pattern은 레코드 객체를 분해해주는 기능이라고 볼 수 있다.
Record 인스턴스가 중첩되어 있어도 Type Pattern을 모두 적용할 수 있다.
[ JEP 441: Pattern Matching for switch ]
위의 Record Pattern과 함께 진행되었으며, Pattern Matching을 Switch 문에서 사용할 수 있도록 지원한다.
✔ Pattern Matching for switch
- 특정 타입 여부 검사
기존의 switch 문은 특정 타입 여부 검사가 제한적이었기 때문에, instance of + if-else 문법을 사용해야 했음.
자바 21부터는 간결한 방식으로 패턴 매칭을 처리할 수 있게 되었다.
// Prior to Java 21
static String formatter(Object obj) {
String formatted = "unknown";
if (obj instanceof Integer i) {
formatted = String.format("int %d", i);
} else if (obj instanceof Long l) {
formatted = String.format("long %d", l);
} else if (obj instanceof Double d) {
formatted = String.format("double %f", d);
} else if (obj instanceof String s) {
formatted = String.format("String %s", s);
}
return formatted;
}
// As of Java 21
static String formatterPatternSwitch(Object obj) {
return switch (obj) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> obj.toString();
};
}
- null 검사
기존에는 switch 문의 파라미터가 null이면 NPE를 던지므로, 외부에서 null 검사를 수행했어야 함.
자바 21부터는 null에 해당하는 케이스를 switch 문 내부에서 검사할 수 있게 되었다.
// Prior to Java 21
static void testFooBarOld(String s) {
if (s == null) {
System.out.println("Oops!");
return;
}
switch (s) {
case "Foo", "Bar" -> System.out.println("Great");
default -> System.out.println("Ok");
}
}
// As of Java 21
static void testFooBarNew(String s) {
switch (s) {
case null -> System.out.println("Oops");
case "Foo", "Bar" -> System.out.println("Great");
default -> System.out.println("Ok");
}
}
출처: https://mangkyu.tistory.com/308 [MangKyu's Diary:티스토리]
- case 세분화
복잡한 구조 → 보다 읽기 좋은 구조
// As of Java 21
static void testStringOld(String response) {
switch (response) {
case null -> { }
case String s -> {
if (s.equalsIgnoreCase("YES"))
System.out.println("You got it");
else if (s.equalsIgnoreCase("NO"))
System.out.println("Shame");
else
System.out.println("Sorry?");
}
}
}
// As of Java 21
static void testStringNew(String response) {
switch (response) {
case null -> { }
case String s
when s.equalsIgnoreCase("YES") -> {
System.out.println("You got it");
}
case String s
when s.equalsIgnoreCase("NO") -> {
System.out.println("Shame");
}
case String s -> {
System.out.println("Sorry?");
}
}
}
- enum 개선
// Prior to Java 21
public enum Suit { DIAMONDS, HEARTS }
static void testforHearts(Suit s) {
switch (s) {
case HEARTS -> System.out.println("It's a heart!");
default -> System.out.println("Some other suit");
}
}
// As of Java 21
sealed interface CardClassification permits Suit, Tarot {}
public enum Suit implements CardClassification { DIAMONDS, HEARTS }
final class Tarot implements CardClassification {}
static void exhaustiveSwitchWithBetterEnumSupport(CardClassification c) {
switch (c) {
case Suit.DIAMONDS -> {
System.out.println("It's diamonds");
}
case Suit.HEARTS -> {
System.out.println("It's hearts");
}
case Tarot t -> {
System.out.println("It's a tarot");
}
}
}
[ JEP 439: Generational ZGC ]
ZGC(Z Garbage Collector)는 새롭게 등장한 Java의 Garbage collector
가장 최근까지 사용되던 G1 GC는 메모리를 region이라는 논리적인 단위로 구분했다.
이 번에 기본 Garbage Collector로 채택된 ZGC는 메모리를 ZPage라는 논리적인 단위로 구분한다.
[ JEP 451: Prepare to Disallow the Dynamic Loading of Agents ]
자바는 agent를 통해 애플리케이션 코드를 동적으로 변경하도록 지원해 옴.
애플리케이션을 모니터링하는 많은 방법들이 있다. (대표적으로 Pinpoint)
자바 애플리케이션을 프로파일링할 때, 정상적인 방법들은 애플리케이션이 실행될 때 호출되고, 실행되는 중간에 호출되는 경우는 거의 없다.
자바 21에서는 실행 중인 JVM에 에이전트가 동적으로 로드될 시, 경고를 발행시킨다.
[ JEP 452: Key Encapsulation Mechanism API ]
공개 키 암호화를 사용하여 대칭 키를 보호하는 암호화 기술인 KEM API가 도입되었다.
- 기존
- 무작위로 생성된 대칭 키를 공개 키로 암호화
- 패딩이 필요하고 보안 증명이 어려울 수 있음
- KEM
- 공개 키의 속성을 사용하여 패딩이 필요 없음
- 양자 공격을 방어하기 위한 핵심 도구가 될 수 있다.
package javax.crypto;
public class DecapsulateException extends GeneralSecurityException;
public final class KEM {
public static KEM getInstance(String alg)
throws NoSuchAlgorithmException;
public static KEM getInstance(String alg, Provider p)
throws NoSuchAlgorithmException;
public static KEM getInstance(String alg, String p)
throws NoSuchAlgorithmException, NoSuchProviderException;
public static final class Encapsulated {
public Encapsulated(SecretKey key, byte[] encapsulation, byte[] params);
public SecretKey key();
public byte[] encapsulation();
public byte[] params();
}
public static final class Encapsulator {
String providerName();
int secretSize(); // Size of the shared secret
int encapsulationSize(); // Size of the key encapsulation message
Encapsulated encapsulate();
Encapsulated encapsulate(int from, int to, String algorithm);
}
public Encapsulator newEncapsulator(PublicKey pk)
throws InvalidKeyException;
public Encapsulator newEncapsulator(PublicKey pk, SecureRandom sr)
throws InvalidKeyException;
public Encapsulator newEncapsulator(PublicKey pk, AlgorithmParameterSpec spec,
SecureRandom sr)
throws InvalidAlgorithmParameterException, InvalidKeyException;
public static final class Decapsulator {
String providerName();
int secretSize(); // Size of the shared secret
int encapsulationSize(); // Size of the key encapsulation message
SecretKey decapsulate(byte[] encapsulation) throws DecapsulateException;
SecretKey decapsulate(byte[] encapsulation, int from, int to,
String algorithm)
throws DecapsulateException;
}
public Decapsulator newDecapsulator(PrivateKey sk)
throws InvalidKeyException;
public Decapsulator newDecapsulator(PrivateKey sk, AlgorithmParameterSpec spec)
throws InvalidAlgorithmParameterException, InvalidKeyException;
}
Bug Fixed
JDK 17과 비교하여 JDK 21까지 수정된 버그들
Double.toString(double) 은 최대한 작은 자릿수를 포함하는 실수를 문자열로 변환하도록 설계되었지만, 실제로 그렇지 않은 상황들이 존재
JDK 19에서 이를 업데이트,
이전 JDK LTS 버전에서 "9.99999999999999E22" 를 반환했다면, JDK 19부터는 "1.0E23"이 반환된다.
IdentityHashMap 의 메소드 remove(key, value)와 replace(key, valye, newValue)는 value 값을 equals()로 비교함 (*Identity에 반함)
이 때문에 해당 String를 문자열 값(compared by identity) 이 아닌,
객체의 주소 값(compared by equality)으로 비교하여 원하는 동작을 하지 않았음
JDK 20에서 이를 "Identity" 기반 비교로 수정하여
이후부터는 IdentityHashMap의 Value 값을 비교할 때 의도된 동작을 하도록 수정되었다.
:: 참고 자료 ::
https://openjdk.org/projects/jdk/21/
JDK 21
JDK 21 This release is the Reference Implementation of version 21 of the Java SE Platform, as specified by JSR 396 in the Java Community Process. JDK 21 reached General Availability on 19 September 2023. Production-ready binaries under the GPL are avai
openjdk.org
https://www.infoq.com/news/2023/09/java-21-so-far/
JDK 21 and JDK 22: What We Know So Far
JDK 21, the next Long-Term Support (LTS) release since JDK 17, has reached its initial release candidate phase with a final set of 15 new features, in the form of JEPs, that can be separated into four categories: Core Java Library, Java Language Specificat
www.infoq.com
JDK 17 ~ 21 Release, 제대로 이해하기
본 포스팅은 openJDK 17 부터 21 까지의 변경 사항을 확인하고, JDK 21을 사용할 때 개발자로서 알아두면 좋을 내용을 학습합니다. Previous Series: 📌 JDK 11 ~ 17 Release, 제대로 이해하기 👉🏻 JDK 17 ~ 21 Rel
gngsn.tistory.com
