JDK 22의 방향
2024년 3월 출시된 JDK 22는 비-LTS 릴리스로, 개발자 편의성 개선에 집중한 릴리스입니다. JDK 21에서 프리뷰였던 Unnamed Variables가 정식 출시되었고, 여러 파일로 구성된 Java 프로그램을 컴파일 없이 바로 실행하는 기능이 추가되었습니다. 또한 Stream API에 커스텀 중간 연산을 정의할 수 있는 Gatherers가 프리뷰로 등장했습니다.
Unnamed Variables & Patterns (정식, JEP 456)
JDK 21에서 프리뷰였던 기능이 정식으로 확정되었습니다. 사용하지 않는 변수에 _(언더스코어)를 사용하여 “이 값은 의도적으로 무시한다”는 것을 명시합니다.
import java.util.List;
import java.util.Map;
public class UnnamedVariablesExample {
sealed interface Event permits Click, Scroll, KeyPress {}
record Click(int x, int y, String target) implements Event {}
record Scroll(int delta) implements Event {}
record KeyPress(char key, boolean ctrl, boolean shift) implements Event {}
public static void main(String[] args) {
List<Event> events = List.of(
new Click(100, 200, "button"),
new Scroll(3),
new KeyPress('S', true, false),
new Click(50, 75, "link")
);
// switch에서 사용하지 않는 필드를 _로 표시
for (Event event : events) {
switch (event) {
// x, y 좌표는 무시하고 target만 사용
case Click(var _, var _, var target) ->
System.out.println("클릭: " + target);
case Scroll(var delta) ->
System.out.println("스크롤: " + delta);
// ctrl, shift는 무시
case KeyPress(var key, var _, var _) ->
System.out.println("키 입력: " + key);
}
}
// 출력:
// 클릭: button
// 스크롤: 3
// 키 입력: S
// 클릭: link
// try-with-resources에서 변수명 불필요한 경우
// 예: 락 획득만 하고 변수 자체를 참조하지 않는 경우
// try (var _ = acquireLock()) { ... }
// enhanced for에서 인덱스 역할 (값은 불필요)
int count = 0;
for (var _ : events) {
count++;
}
System.out.println("총 이벤트 수: " + count);
// 출력: 총 이벤트 수: 4
// Map 순회에서 키 또는 값 무시
Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 87, "Charlie", 92);
int total = 0;
for (var entry : scores.entrySet()) {
// 키는 무시하고 값만 합산하는 경우에도
// Map.Entry에서는 직접 _를 쓸 수 없으므로 getValue() 활용
total += entry.getValue();
}
System.out.println("점수 합계: " + total);
// 출력: 점수 합계: 274
}
}
_는 같은 스코프에서 여러 번 사용할 수 있습니다. case Click(var _, var _, var target)처럼 두 개의 _가 동시에 등장해도 문제가 없습니다. 기존에는 사용하지 않는 변수에 ignored, unused 같은 이름을 붙여야 했는데, _로 통일되어 코드 의도가 명확해졌습니다.
Launch Multi-File Source-Code Programs (JEP 458)
JDK 11에서 도입된 단일 파일 실행(java Hello.java)이 여러 파일로 확장되었습니다. 컴파일 단계 없이 java Main.java만으로 같은 디렉토리의 다른 Java 파일을 자동으로 찾아 함께 실행합니다.
// --- 파일: Calculator.java ---
public class Calculator {
// 기본 사칙연산
public static int add(int a, int b) { return a + b; }
public static int subtract(int a, int b) { return a - b; }
public static int multiply(int a, int b) { return a * b; }
public static double divide(int a, int b) {
if (b == 0) throw new ArithmeticException("0으로 나눌 수 없습니다");
return (double) a / b;
}
}
// --- 파일: Main.java ---
public class Main {
public static void main(String[] args) {
// Calculator.java를 별도로 컴파일할 필요 없음
System.out.println("10 + 3 = " + Calculator.add(10, 3));
System.out.println("10 - 3 = " + Calculator.subtract(10, 3));
System.out.println("10 * 3 = " + Calculator.multiply(10, 3));
System.out.println("10 / 3 = " + Calculator.divide(10, 3));
}
}
// 실행: java Main.java
// 출력:
// 10 + 3 = 13
// 10 - 3 = 7
// 10 * 3 = 30
// 10 / 3 = 3.3333333333333335
실행 방법은 java Main.java 한 줄이면 됩니다. javac로 컴파일하는 단계가 필요 없습니다. 프로토타이핑, 교육, 스크립팅 목적에 특히 유용합니다. 단, 프로덕션 프로젝트에서는 여전히 빌드 도구(Gradle, Maven)를 사용하는 것이 권장됩니다.
Stream Gatherers (Preview, JEP 461)
Stream API에 커스텀 중간 연산을 정의할 수 있는 gather() 메서드가 프리뷰로 추가되었습니다. 기존에는 filter, map, flatMap 등 미리 정의된 연산만 사용할 수 있었지만, Gatherers를 사용하면 윈도우 슬라이딩, 중복 제거, 상태 기반 변환 등 커스텀 로직을 중간 연산으로 정의할 수 있습니다.
import java.util.List;
import java.util.stream.Gatherers;
import java.util.stream.Stream;
public class GathererExample {
// 실행 시 --enable-preview 필요
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// windowFixed: 고정 크기 윈도우로 그룹핑
List<List<Integer>> windows = numbers.stream()
.gather(Gatherers.windowFixed(3))
.toList();
System.out.println("고정 윈도우 (크기 3): " + windows);
// 출력: 고정 윈도우 (크기 3): [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
// windowSliding: 슬라이딩 윈도우 (이동 평균 계산에 활용)
List<Double> movingAvg = numbers.stream()
.gather(Gatherers.windowSliding(3))
.map(window -> window.stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0))
.toList();
System.out.println("이동 평균 (윈도우 3): " + movingAvg);
// 출력: 이동 평균 (윈도우 3): [2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
// fold: 누적 결과를 스트림으로 방출
List<Integer> runningSum = numbers.stream()
.gather(Gatherers.fold(() -> 0, Integer::sum))
.toList();
System.out.println("누적 합계: " + runningSum);
// 출력: 누적 합계: [55]
// scan: fold와 유사하지만 중간 결과를 모두 방출
List<Integer> scanSum = numbers.stream()
.gather(Gatherers.scan(() -> 0, Integer::sum))
.toList();
System.out.println("단계별 합계: " + scanSum);
// 출력: 단계별 합계: [1, 3, 6, 10, 15, 21, 28, 36, 45, 55]
}
}
Gatherers.windowFixed()와 Gatherers.windowSliding()은 데이터 분석에서 자주 사용되는 패턴이고, scan()은 함수형 프로그래밍의 scanLeft에 해당합니다. 기존에는 이런 연산을 구현하려면 collect()에서 복잡한 커스텀 Collector를 만들거나 for 루프로 돌아가야 했습니다.
Statements before super() (Preview, JEP 447)
생성자에서 super() 호출 전에 검증 로직을 실행할 수 있는 기능입니다. 기존에는 super()가 반드시 생성자의 첫 번째 문장이어야 했습니다.
public class SuperBeforeExample {
// 실행 시 --enable-preview 필요
static class Animal {
final String name;
Animal(String name) {
this.name = name;
System.out.println("Animal 생성: " + name);
}
}
static class Dog extends Animal {
final int age;
Dog(String name, int age) {
// JDK 22 프리뷰: super() 호출 전에 검증 가능
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("이름은 필수입니다");
}
if (age < 0) {
throw new IllegalArgumentException("나이는 0 이상이어야 합니다");
}
// 검증 후 super 호출
super(name.strip());
this.age = age;
System.out.println("Dog 생성: " + name + ", " + age + "세");
}
}
public static void main(String[] args) {
Dog dog = new Dog(" Buddy ", 3);
System.out.println("이름: '" + dog.name + "', 나이: " + dog.age);
// 출력:
// Animal 생성: Buddy
// Dog 생성: Buddy , 3세
// 이름: 'Buddy', 나이: 3
try {
new Dog("", 5); // 검증 실패
} catch (IllegalArgumentException e) {
System.out.println("에러: " + e.getMessage());
}
// 출력: 에러: 이름은 필수입니다
}
}
기존에는 super() 전에 검증을 넣으려면 정적 팩토리 메서드나 헬퍼 메서드를 만드는 우회 패턴이 필요했습니다. 이 기능으로 생성자 내부에서 자연스럽게 검증 로직을 배치할 수 있게 되었습니다.
Foreign Function & Memory API (정식, JEP 454)
JDK 19부터 프리뷰를 거쳐 드디어 정식 출시된 FFM API입니다. JNI를 대체하여 네이티브 라이브러리를 안전하고 효율적으로 호출할 수 있습니다. Arena로 오프힙 메모리 수명을 관리하고, Linker로 C 함수를 직접 호출합니다.
JDK 20의 FFM API 예제와 동일한 패턴이지만, 정식 출시이므로 --enable-preview 플래그 없이 사용할 수 있습니다.
성능: Region Pinning for G1 (JEP 423)
G1 GC에서 JNI Critical Region 처리가 개선되었습니다. 기존에는 JNI Critical Section이 활성화되면 G1 GC가 전체 컬렉션을 중단해야 했습니다. JDK 22부터는 해당 Region만 고정(pinning)하고 나머지 Region은 정상적으로 수집하므로, JNI를 많이 사용하는 애플리케이션에서 GC 일시정지 시간이 크게 감소합니다.
# G1 GC 사용 시 자동 적용 (별도 설정 불필요)
java -XX:+UseG1GC -jar app.jar
# GC 로그로 Region Pinning 효과 확인
java -XX:+UseG1GC -Xlog:gc* -jar app.jar
JNI 라이브러리를 사용하는 Apache Spark, Hadoop, TensorFlow Java 등의 환경에서 체감 효과가 큽니다.
Class-File API (Preview, JEP 457)
바이트코드를 읽고 쓰고 변환하는 표준 API가 프리뷰로 추가되었습니다. 기존에는 ASM, ByteBuddy 같은 서드파티 라이브러리에 의존해야 했지만, JDK에 내장된 표준 API로 바이트코드 조작이 가능해집니다.
이 API는 주로 프레임워크와 도구 개발자를 위한 것으로, 일반 애플리케이션 개발에서 직접 사용할 일은 드뭅니다. 하지만 Spring, Hibernate 등의 프레임워크가 내부적으로 이 API를 활용하게 되면, 서드파티 바이트코드 라이브러리 의존성이 줄어들고 JDK 업그레이드 시 호환성 문제가 감소합니다.
실전 팁
JDK 22를 실무에 적용할 때 알아두면 좋은 점을 정리합니다.
| 기능 | 상태 | 권장 사용 환경 |
|---|---|---|
Unnamed Variables (_) | 정식 | 즉시 적용 가능 |
| Multi-File Launch | 정식 | 프로토타이핑, 교육, 스크립팅 |
| FFM API | 정식 | JNI 대체 시 적극 활용 |
| Stream Gatherers | Preview | 테스트 환경에서 시험 |
| Statements before super() | Preview | 테스트 환경에서 시험 |
| Class-File API | Preview | 프레임워크/도구 개발자용 |
| Region Pinning (G1) | 정식 | JNI 사용 시 자동 적용 |
JDK 22는 “혁신적인 대형 기능”보다는 “개발자가 매일 쓰는 코드를 더 편하게” 만드는 데 집중한 릴리스입니다. _로 의도를 명확히 하고, 여러 파일을 컴파일 없이 실행하고, Stream에 커스텀 연산을 추가하는 것 모두 일상적인 개발 경험을 개선하는 변화입니다. 비-LTS 릴리스이지만, 정식 출시된 기능(Unnamed Variables, FFM API, Region Pinning)은 JDK 23 이후에서도 그대로 유지되므로 안심하고 익혀두면 됩니다.