Java 21 핵심 기능 정리 (LTS)
Java 21 LTS의 Virtual Threads 표준화, Pattern Matching Switch, Record Patterns, Sequenced Collections, Scoped Values Preview, String Templates Preview를 상세히 정리합니다.
지난 글에서 Java 18~20의 변화를 살펴봤습니다. Java 21은 2023년 9월 출시된 LTS 버전으로, Java 11 이후 가장 많은 언어 및 플랫폼 기능이 표준화된 버전입니다. Virtual Threads의 표준화로 동시성 프로그래밍이 단순해지고, Pattern Matching 완성으로 타입 안전 코드가 간결해졌습니다.
Virtual Threads 표준화 (JEP 444)
Java 19·20에서 Preview를 거쳐 Java 21에서 표준화됩니다. --enable-preview 없이 사용 가능합니다.
// 방법 1: Thread.ofVirtual()
Thread vt = Thread.ofVirtual()
.name("request-handler-", 0)
.start(() -> handleRequest(request));
// 방법 2: ExecutorService
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000)
.forEach(i -> executor.submit(() -> processTask(i)));
} // try-with-resources로 자동 종료
// 방법 3: Spring Boot 3.2+
// application.properties
// spring.threads.virtual.enabled=true
// → Tomcat, @Async, @Scheduled 자동 전환
Virtual Thread는 Thread.isVirtual()로 구분할 수 있습니다.
Thread t = Thread.currentThread();
System.out.println(t.isVirtual()); // true (VT 내에서)
Pattern Matching for switch 표준화 (JEP 441)
Java 17~20에서 Preview를 거쳐 표준화됩니다.
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
record Triangle(double base, double height) implements Shape {}
// Sealed + switch → 완전성 컴파일 검증
double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.w() * r.h();
case Triangle t -> 0.5 * t.base() * t.height();
// default 불필요 — 모든 case 커버됨
};
}
// when 절 (Guarded Pattern)
String classify(Object obj) {
return switch (obj) {
case null -> "null";
case Integer i when i < 0 -> "음수";
case Integer i -> "정수";
case String s when s.isBlank() -> "빈 문자열";
case String s -> "문자열";
default -> "기타";
};
}
중요한 순서 규칙: when이 있는 패턴(guarded)이 동일 타입의 일반 패턴(unguarded)보다 앞에 위치해야 합니다. 그렇지 않으면 컴파일 오류가 발생합니다.
Record Patterns 표준화 (JEP 440)
record Point(int x, int y) {}
record ColoredPoint(Point p, Color color) {}
// instanceof + Record Pattern
void printCoordinate(Object obj) {
if (obj instanceof Point(int x, int y)) {
System.out.printf("x=%d, y=%d%n", x, y);
}
}
// switch + 중첩 Record Pattern
String describe(Object obj) {
return switch (obj) {
case ColoredPoint(Point(int x, int y), Color c) ->
"(%d,%d) in %s".formatted(x, y, c);
case Point(int x, int y) ->
"(%d,%d)".formatted(x, y);
default -> obj.toString();
};
}
// var 패턴 — 타입 추론
if (obj instanceof Point(var x, var y)) {
System.out.println(x + ", " + y);
}
Sequenced Collections (JEP 431)
기존 컬렉션에서 첫/마지막 원소를 가져오는 방법이 컬렉션마다 달랐습니다. SequencedCollection 인터페이스가 이를 통일합니다.
// 이전 — 컬렉션마다 다른 방법
List<Integer> list = new ArrayList<>(List.of(1, 2, 3));
list.get(0); // 첫 원소 (List)
list.get(list.size()-1); // 마지막 원소 (List)
LinkedList<Integer> deque = new LinkedList<>(List.of(1,2,3));
deque.getFirst(); // LinkedList/Deque
deque.getLast(); // LinkedList/Deque
// Java 21 — 통일된 인터페이스
list.getFirst(); // 1 (NoSuchElementException if empty)
list.getLast(); // 3
list.reversed(); // [3, 2, 1] (뷰 — 원본 공유)
// addFirst / addLast / removeFirst / removeLast
list.addFirst(0); // [0, 1, 2, 3]
list.removeLast(); // [0, 1, 2]
SequencedMap도 추가됩니다.
var map = new LinkedHashMap<>(Map.of("a", 1, "b", 2, "c", 3));
map.firstEntry(); // Map.Entry<K, V>
map.lastEntry();
map.reversed(); // 역순 SequencedMap 뷰
Scoped Values Preview (JEP 446)
ThreadLocal의 문제점(가변, Virtual Thread에서 수백만 복사본, 부모→자식 상속)을 해결하는 ScopedValue가 Preview로 도입됩니다.
static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
void handle(HttpRequest req) {
User user = authenticate(req);
ScopedValue.where(CURRENT_USER, user).run(() -> {
processOrder();
sendNotification();
});
}
void processOrder() {
User user = CURRENT_USER.get(); // 현재 범위의 값
// ...
}
ScopedValue의 특징:
- 불변: 한번 설정하면
where범위 내에서 변경 불가 - 범위 한정:
run()또는call()블록 종료 시 자동 해제 - Virtual Thread 최적화: 수백만 VT가 동일
ScopedValue를 공유해도 복사본 없음
String Templates Preview (JEP 430)
문자열 보간을 안전하게 처리하는 Template Processor가 Preview로 도입됩니다.
// --enable-preview 필요
String name = "Alice";
int age = 30;
// STR 프로세서 — 단순 보간
String msg = STR."Hello, \{name}! You are \{age} years old.";
// FMT 프로세서 — 포맷 지정
String formatted = FMT."Balance: \{"%.2f".formatted(balance)}";
// 커스텀 프로세서 — SQL 이스케이핑 등
String safeSql = SQL."SELECT * FROM users WHERE name = \{userInput}";
// SQL 프로세서가 userInput을 자동으로 파라미터 바인딩
주의: String Templates는 이후 버전에서 재설계 결정이 내려져 Java 23에서 Preview 취소됩니다. 프로덕션 코드에 사용하지 않는 것을 권장합니다.
Unnamed Patterns and Variables Preview (JEP 443)
_를 미사용 변수 자리에 사용할 수 있습니다.
// 미사용 변수 _ 표기
try {
processRequest();
} catch (Exception _) { // 예외 변수 불필요
log.error("Request failed");
}
// Unnamed Pattern in switch
switch (shape) {
case Circle c -> draw(c);
case _ -> skip(); // 나머지 모든 타입
}
// Pattern에서 필요 없는 컴포넌트 무시
if (obj instanceof Point(int x, _)) {
System.out.println("x=" + x); // y는 불필요
}
Unnamed Classes and Instance Main Methods Preview (JEP 445)
학습·스크립팅 목적으로 public static void main(String[] args) 없이 실행 가능합니다.
// Java 21 Preview — 클래스 선언 불필요
void main() {
System.out.println("Hello, Java 21!");
}
Java 17 → 21 마이그레이션 요점
대부분의 Java 17 코드는 수정 없이 21에서 동작합니다. 주요 확인 사항:
# 1. Virtual Threads 적용 (선택)
# Spring Boot 3.2+
spring.threads.virtual.enabled=true
# 2. Sequenced Collections API 사용 (선택적 리팩토링)
# list.get(0) → list.getFirst()
# 3. Deprecated API 정리
# Security Manager 관련 코드 제거
# Finalization 의존 코드 → try-with-resources
# 4. Pattern Matching switch로 instanceof 체인 단순화
Java 21은 2031년까지 지원되는 LTS입니다. Java 8/11에서 21로 마이그레이션하는 것이 현 시점의 가장 권장되는 업그레이드 경로입니다.
지난 글: Java 18~20 브리지 — Virtual Threads Preview와 UTF-8 기본화
다음 글: Java LTS 마이그레이션 로드맵
읽어주셔서 감사합니다. 😊