JDK 20의 위치
2023년 3월 출시된 JDK 20은 비-LTS 릴리스입니다. 단독으로 보면 화려한 신기능이 적어 보이지만, 실제로는 JDK 21 LTS를 위한 최종 리허설 역할을 합니다. Record Patterns, Virtual Threads, Structured Concurrency 등 JDK 19에서 첫 프리뷰로 등장한 기능들이 두 번째 프리뷰를 거치며 API가 다듬어졌고, 이 과정에서 커뮤니티 피드백이 반영되었습니다.
이 글에서는 JDK 20에 포함된 주요 JEP(JDK Enhancement Proposal)을 실행 가능한 예제와 함께 정리합니다.
Record Patterns (Second Preview, JEP 432)
Record Patterns는 instanceof나 switch에서 레코드 타입을 구조 분해(destructuring) 하는 기능입니다. JDK 19에서 첫 프리뷰로 등장했고, JDK 20에서는 중첩 레코드 분해와 제네릭 레코드 지원이 강화되었습니다.
public class RecordPatternExample {
// 좌표를 표현하는 레코드
record Point(int x, int y) {}
// 선분을 표현하는 중첩 레코드
record Line(Point start, Point end) {}
// 제네릭 레코드
record Pair<T>(T first, T second) {}
static String describeLine(Object obj) {
// 중첩 레코드 분해: Line 안의 Point까지 한 번에 추출
if (obj instanceof Line(Point(var x1, var y1), Point(var x2, var y2))) {
double length = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
return "선분 길이: %.2f".formatted(length);
}
return "Line이 아닙니다";
}
public static void main(String[] args) {
Line line = new Line(new Point(0, 0), new Point(3, 4));
System.out.println(describeLine(line));
// 출력: 선분 길이: 5.00
// 제네릭 레코드 패턴 매칭
Pair<String> pair = new Pair<>("Hello", "World");
if (pair instanceof Pair<String>(var first, var second)) {
System.out.println(first + " " + second);
}
// 출력: Hello World
}
}
기존에는 instanceof로 타입을 확인한 뒤 캐스팅하고, getter를 호출하는 3단계가 필요했습니다. Record Patterns를 사용하면 한 줄에서 타입 확인과 필드 추출이 동시에 이루어집니다. 중첩 레코드까지 한 번에 분해할 수 있어 DTO나 도메인 객체 처리가 훨씬 깔끔해집니다.
Pattern Matching for switch (Fourth Preview, JEP 433)
switch 문에서 타입 패턴과 when 가드를 사용하는 기능의 네 번째 프리뷰입니다. JDK 20에서는 문법이 더욱 안정화되어 JDK 21 정식 릴리스를 앞두고 있습니다.
public class SwitchPatternExample {
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Triangle(double base, double height) implements Shape {}
// switch 패턴 매칭 + Record Patterns + when 가드 조합
static String describeShape(Shape shape) {
return switch (shape) {
case Circle(var r) when r > 100 -> "큰 원 (반지름: %.1f)".formatted(r);
case Circle(var r) -> "원 (반지름: %.1f)".formatted(r);
case Rectangle(var w, var h) when w == h -> "정사각형 (변: %.1f)".formatted(w);
case Rectangle(var w, var h) -> "직사각형 (%.1f x %.1f)".formatted(w, h);
case Triangle(var b, var h) -> "삼각형 (밑변: %.1f, 높이: %.1f)".formatted(b, h);
};
}
public static void main(String[] args) {
Shape[] shapes = {
new Circle(5.0),
new Circle(150.0),
new Rectangle(10.0, 10.0),
new Rectangle(3.0, 7.0),
new Triangle(6.0, 4.0)
};
for (Shape s : shapes) {
System.out.println(describeShape(s));
}
// 출력:
// 원 (반지름: 5.0)
// 큰 원 (반지름: 150.0)
// 정사각형 (변: 10.0)
// 직사각형 (3.0 x 7.0)
// 삼각형 (밑변: 6.0, 높이: 4.0)
}
}
sealed interface와 패턴 매칭을 함께 사용하면 컴파일러가 모든 하위 타입을 검사하므로 default 분기가 필요 없습니다. 새로운 Shape 구현을 추가하면 컴파일 에러로 즉시 알려줍니다.
Scoped Values (Incubator, JEP 429)
ScopedValue는 ThreadLocal의 대안으로, 특정 스코프 내에서만 값을 공유하는 메커니즘입니다. Virtual Thread 환경에서 ThreadLocal의 메모리 문제를 해결하기 위해 설계되었습니다.
ThreadLocal과의 핵심 차이점은 다음과 같습니다.
| 항목 | ThreadLocal | ScopedValue |
|---|---|---|
| 수명 | 스레드 전체 | 지정된 스코프 내 |
| 변경 | 언제든 가능 (mutable) | 한 번 바인딩 후 불변 |
| 상속 | InheritableThreadLocal 필요 | 자동 상속 (Structured Concurrency와 통합) |
| 메모리 | 스레드당 누적 | 스코프 종료 시 자동 해제 |
import java.util.concurrent.Executors;
public class ScopedValueExample {
// ScopedValue 선언 (JDK 20 incubator)
// 실행 시 --enable-preview --add-modules jdk.incubator.concurrent 필요
static final ScopedValue<String> CURRENT_USER = ScopedValue.newInstance();
static void processRequest() {
// ScopedValue에서 값 읽기
String user = CURRENT_USER.get();
System.out.println("요청 처리 중 - 사용자: " + user);
handleDatabase();
}
static void handleDatabase() {
// 호출 스택 어디서든 접근 가능 (파라미터 전달 불필요)
String user = CURRENT_USER.get();
System.out.println("DB 쿼리 실행 - 사용자: " + user);
}
public static void main(String[] args) throws Exception {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// 각 요청마다 ScopedValue 바인딩
executor.submit(() -> {
ScopedValue.where(CURRENT_USER, "alice")
.run(() -> processRequest());
return null;
});
executor.submit(() -> {
ScopedValue.where(CURRENT_USER, "bob")
.run(() -> processRequest());
return null;
});
}
// 출력:
// 요청 처리 중 - 사용자: alice
// DB 쿼리 실행 - 사용자: alice
// 요청 처리 중 - 사용자: bob
// DB 쿼리 실행 - 사용자: bob
}
}
ScopedValue.where(KEY, value).run(...) 패턴으로 값을 바인딩하면, run() 블록 내의 모든 코드에서 KEY.get()으로 값에 접근할 수 있습니다. 블록이 종료되면 바인딩이 자동으로 해제되어 메모리 누수가 발생하지 않습니다.
Virtual Threads (Second Preview)와 Structured Concurrency
Virtual Threads(JEP 436)는 JDK 19의 첫 프리뷰에서 API가 거의 확정된 상태였고, JDK 20에서는 소폭의 개선만 이루어졌습니다. Virtual Threads의 상세한 사용법과 주의사항은 Virtual Threads 완벽 가이드 포스트를 참고하세요.
Structured Concurrency(JEP 437)는 여러 동시 작업을 하나의 단위로 묶어 관리하는 API입니다. 부모 작업이 취소되면 자식 작업도 자동으로 취소되고, 자식의 예외가 부모로 전파됩니다. 마치 코드의 구조적 블록(try-finally)처럼 동시성을 관리하는 개념입니다.
Foreign Function & Memory API (Second Preview, JEP 434)
JNI(Java Native Interface)를 대체하기 위한 FFM API의 두 번째 프리뷰입니다. 네이티브 라이브러리 호출과 오프힙 메모리 관리를 안전하고 효율적으로 수행할 수 있습니다.
import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.SymbolLookup;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle;
public class FFMExample {
public static void main(String[] args) throws Throwable {
// 오프힙 메모리 할당 및 관리
try (Arena arena = Arena.ofConfined()) {
// C 문자열을 오프힙 메모리에 할당
MemorySegment cString = arena.allocateFrom("Hello from Java FFM!");
// 네이티브 strlen 함수 호출
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();
MethodHandle strlen = linker.downcallHandle(
stdlib.find("strlen").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);
long length = (long) strlen.invoke(cString);
System.out.println("문자열 길이: " + length);
// 출력: 문자열 길이: 20
}
// Arena 종료 시 오프힙 메모리 자동 해제
}
}
Arena가 메모리 수명 주기를 관리하므로, try-with-resources로 감싸면 네이티브 메모리 누수를 방지할 수 있습니다. JNI에서 수동으로 free()를 호출하던 것에 비해 안전성이 크게 향상되었습니다.
정리
JDK 20은 독립적으로 보면 “프리뷰만 가득한 릴리스”로 느껴질 수 있지만, JDK 21 LTS를 위한 최종 안정화 과정이었습니다.
| JEP | 기능 | 상태 | JDK 21 진행 |
|---|---|---|---|
| 432 | Record Patterns | 2nd Preview | → 정식 (JEP 440) |
| 433 | Pattern Matching for switch | 4th Preview | → 정식 (JEP 441) |
| 429 | Scoped Values | Incubator | → Preview (JEP 446) |
| 436 | Virtual Threads | 2nd Preview | → 정식 (JEP 444) |
| 437 | Structured Concurrency | 2nd Incubator | → Preview (JEP 453) |
| 434 | Foreign Function & Memory API | 2nd Preview | → 3rd Preview (JEP 442) |
JDK 20에서 프리뷰 기능을 미리 경험해 본 개발자라면 JDK 21 마이그레이션이 훨씬 수월합니다. 특히 Record Patterns와 Pattern Matching for switch는 JDK 20 → 21 사이에 문법 변경이 거의 없어, JDK 20에서 작성한 코드를 그대로 사용할 수 있습니다. 비-LTS 릴리스라도 프로덕션 적용을 고려하지 않는 개발/테스트 환경에서는 적극적으로 활용해보는 것을 권장합니다.