역대 최다 24개 JEP
2025년 3월 18일 출시된 JDK 24는 24개 JEP을 탑재하며 역대 가장 많은 변경사항을 포함한 릴리스입니다. Stream Gatherers의 정식화, Virtual Thread 핀닝 문제 해결, 양자내성 암호화 도입 등 실무에 즉시 영향을 주는 기능이 다수 포함되었습니다.
이 글에서는 그중 가장 중요한 기능을 실행 가능한 예제와 함께 정리합니다.
Stream Gatherers (JEP 485, 정식)
JDK 22에서 프리뷰로 등장한 Stream Gatherers가 JDK 24에서 드디어 정식 확정되었습니다. 기존 Stream API의 map, filter, flatMap은 상태 없는(stateless) 변환에 특화되어 있었지만, 윈도우 처리나 누적 변환 같은 상태 기반 연산은 표현하기 어려웠습니다.
Gatherers 유틸리티 클래스는 자주 쓰이는 패턴을 미리 제공합니다.
import java.util.List;
import java.util.stream.Gatherers;
import java.util.stream.Stream;
public class StreamGatherersDemo {
public static void main(String[] args) {
List<Integer> data = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// windowFixed(n): n개씩 고정 크기 윈도우로 분할
List<List<Integer>> fixedWindows = data.stream()
.gather(Gatherers.windowFixed(3))
.toList();
System.out.println("고정 윈도우(3): " + fixedWindows);
// 출력: 고정 윈도우(3): [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
// windowSliding(n): 슬라이딩 윈도우 — 이동 평균 등에 유용
List<List<Integer>> slidingWindows = data.stream()
.gather(Gatherers.windowSliding(4))
.toList();
System.out.println("슬라이딩 윈도우(4): " + slidingWindows);
// 출력: 슬라이딩 윈도우(4): [[1, 2, 3, 4], [2, 3, 4, 5], ..., [7, 8, 9, 10]]
// scan(): 누적 값을 스트림으로 생성 (누적합 등)
List<Integer> cumSum = data.stream()
.gather(Gatherers.scan(() -> 0, Integer::sum))
.toList();
System.out.println("누적합: " + cumSum);
// 출력: 누적합: [1, 3, 6, 10, 15, 21, 28, 36, 45, 55]
// fold(): 최종 누적 값만 반환 (reduce와 유사하지만 초기값 팩토리 사용)
List<Integer> total = data.stream()
.gather(Gatherers.fold(() -> 0, Integer::sum))
.toList();
System.out.println("전체 합계: " + total);
// 출력: 전체 합계: [55]
// 실전 예: 슬라이딩 윈도우로 이동 평균 계산
List<Double> movingAvg = data.stream()
.gather(Gatherers.windowSliding(3))
.map(window -> window.stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0.0))
.toList();
System.out.println("3일 이동평균: " + movingAvg);
// 출력: 3일 이동평균: [2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
}
}
windowFixed()와 windowSliding()의 차이를 정리하면: windowFixed는 데이터를 겹치지 않는 블록으로 나누고, windowSliding은 한 칸씩 밀면서 겹치는 블록을 생성합니다. 시계열 데이터의 이동 평균 계산에는 windowSliding이 적합합니다.
Virtual Thread 핀닝 해결 (JEP 491, 정식)
JDK 21에서 도입된 Virtual Thread에는 핀닝(Pinning) 문제가 있었습니다. synchronized 블록 안에서 블로킹 I/O를 수행하면, Virtual Thread가 Carrier Thread에 “고정”되어 다른 Virtual Thread가 실행되지 못하는 현상입니다.
JDK 24의 JEP 491은 이 문제를 JVM 레벨에서 해결합니다. synchronized 블록 안에서도 Virtual Thread가 정상적으로 yield됩니다.
import java.util.concurrent.Executors;
import java.util.concurrent.CountDownLatch;
public class VirtualThreadPinningFixed {
// 공유 자원을 보호하는 동기화 객체
private static final Object lock = new Object();
private static int counter = 0;
public static void main(String[] args) throws InterruptedException {
int taskCount = 100;
CountDownLatch latch = new CountDownLatch(taskCount);
long start = System.currentTimeMillis();
// Virtual Thread로 100개 작업 동시 실행
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < taskCount; i++) {
executor.submit(() -> {
synchronized (lock) {
// JDK 23 이전: 이 synchronized 안에서 sleep하면 Carrier Thread가 핀닝됨
// JDK 24: 핀닝 없이 정상적으로 다른 Virtual Thread에 양보
try {
Thread.sleep(10); // I/O 대기 시뮬레이션
counter++;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
latch.countDown();
});
}
}
latch.await();
long elapsed = System.currentTimeMillis() - start;
System.out.println("처리된 작업 수: " + counter);
System.out.println("총 소요 시간: " + elapsed + "ms");
System.out.println("핀닝 여부: JDK 24에서는 발생하지 않음");
// 출력 예시:
// 처리된 작업 수: 100
// 총 소요 시간: 1050ms
// 핀닝 여부: JDK 24에서는 발생하지 않음
}
}
이 변화가 중요한 이유는 기존 Java 라이브러리 대부분이 synchronized를 사용하고 있기 때문입니다. JDK 23 이전에는 이런 라이브러리를 Virtual Thread와 함께 사용하면 성능이 오히려 저하될 수 있었습니다. JDK 24부터는 기존 코드 수정 없이 Virtual Thread의 이점을 온전히 누릴 수 있습니다.
양자내성 암호화 (JEP 496/497, 정식)
양자 컴퓨터가 실용화되면 현재 널리 쓰이는 RSA, ECDSA 같은 암호화 알고리즘이 깨질 수 있습니다. JDK 24는 미국 NIST가 표준화한 양자내성(Post-Quantum) 암호화 알고리즘 두 가지를 정식 지원합니다.
| JEP | 알고리즘 | 용도 |
|---|---|---|
| JEP 496 | ML-KEM (Module-Lattice Key Encapsulation) | 키 교환 |
| JEP 497 | ML-DSA (Module-Lattice Digital Signature) | 전자 서명 |
import java.security.KeyPairGenerator;
import java.security.KeyPair;
import java.security.Signature;
public class QuantumResistantDemo {
public static void main(String[] args) throws Exception {
// ML-DSA: 양자내성 전자 서명 알고리즘
// NIST FIPS 204 표준 기반
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("ML-DSA");
keyGen.initialize(65); // ML-DSA-65 (보안 수준: 128비트 상당)
KeyPair keyPair = keyGen.generateKeyPair();
System.out.println("알고리즘: " + keyPair.getPublic().getAlgorithm());
System.out.println("공개키 크기: " + keyPair.getPublic().getEncoded().length + " bytes");
// 출력 예시:
// 알고리즘: ML-DSA
// 공개키 크기: 1952 bytes
// 서명 생성
Signature signer = Signature.getInstance("ML-DSA");
signer.initSign(keyPair.getPrivate());
byte[] message = "양자내성 서명 테스트".getBytes();
signer.update(message);
byte[] signature = signer.sign();
System.out.println("서명 크기: " + signature.length + " bytes");
// 출력 예시: 서명 크기: 3309 bytes
// 서명 검증
Signature verifier = Signature.getInstance("ML-DSA");
verifier.initVerify(keyPair.getPublic());
verifier.update(message);
boolean valid = verifier.verify(signature);
System.out.println("서명 검증: " + (valid ? "유효" : "무효"));
// 출력: 서명 검증: 유효
}
}
양자내성 암호화의 키와 서명 크기는 기존 RSA/ECDSA보다 큽니다. 이는 수학적 구조가 다르기 때문인데, 격자(Lattice) 기반 문제는 양자 컴퓨터로도 효율적으로 풀 수 없는 것으로 알려져 있습니다. “Harvest Now, Decrypt Later” 공격에 대비하려면 지금부터 양자내성 암호화로의 전환을 준비해야 합니다.
그 외 주목할 JEP
Class-File API (JEP 484, 정식): 바이트코드를 읽고 쓸 수 있는 표준 API입니다. 기존에는 ASM 같은 외부 라이브러리에 의존해야 했지만, 이제 JDK 내장 API로 .class 파일을 생성하고 변환할 수 있습니다. 프레임워크 개발자에게 큰 변화입니다.
AOT Class Loading and Linking (JEP 483): 애플리케이션 시작 시 클래스 로딩과 링킹 결과를 캐시하여, 재시작 시 시작 시간을 단축합니다. 마이크로서비스 환경에서 Cold Start 문제를 완화하는 데 효과적입니다.
Compact Object Headers (JEP 450, Experimental): 객체 헤더 크기를 12바이트에서 8바이트로 줄이는 실험적 기능입니다. JDK 25에서 정식 확정되었으며, 메모리 집약적 애플리케이션에서 힙 사용량을 크게 줄여줍니다.
jlink JMOD 없이 런타임 이미지 (JEP 493): jlink가 JMOD 파일 없이도 런타임 이미지를 생성할 수 있게 되어, JDK 배포 크기가 약 25% 감소합니다. 컨테이너 기반 배포에서 이미지 크기 최적화에 기여합니다.
ZGC 비세대 모드 제거 (JEP 490): JDK 23에서 세대별 모드가 기본값이 된 데 이어, JDK 24에서는 비세대별(Non-Generational) 모드가 완전히 제거되었습니다. -XX:-ZGenerational 옵션은 더 이상 사용할 수 없습니다.
G1 Late Barrier Expansion (JEP 475): G1 GC의 배리어 코드 생성 시점을 JIT 컴파일 후반으로 옮겨, JIT 컴파일러의 최적화 기회를 늘립니다.
실전 팁
| 기능 | 상태 | 핵심 포인트 |
|---|---|---|
| Stream Gatherers | 정식 | windowFixed, windowSliding, scan, fold 내장 |
| Virtual Thread 핀닝 해결 | 정식 | synchronized 안에서도 정상 yield |
| ML-KEM/ML-DSA | 정식 | NIST 표준 양자내성 키교환/서명 |
| Class-File API | 정식 | ASM 없이 바이트코드 조작 |
| AOT Class Loading | 정식 | 재시작 시 클래스 로딩 캐시 |
| Compact Object Headers | 실험적 | 객체 헤더 12B → 8B |
| jlink 경량화 | 정식 | JDK 크기 25% 감소 |
JDK 24에서 가장 먼저 적용해볼 것을 하나 고르자면 Stream Gatherers입니다. 시계열 데이터를 다루거나 배치 처리에서 윈도우 연산이 필요했다면, Gatherers.windowFixed()와 windowSliding()이 외부 라이브러리 없이 문제를 해결해줍니다. Virtual Thread를 사용 중이라면 JDK 24로 업그레이드하는 것만으로도 핀닝 문제가 사라지므로, 성능 개선 효과를 무료로 얻을 수 있습니다.