| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- GCP
- ci/cd
- springboot
- docker
- Flyway
- 백엔드면접준비
- GitHub Packages
- 마이크로서비스
- 멀티모듈
- MSA
- 공통모듈
- dockercompose
- 백엔드
- 마이그레이션
- CS
- Java 8
- 도커
- 자바
- github actions
- SpringCloud
- 마이크로서비스아키텍처
- 인프라
- gradle
- 트러블슈팅
- PostgreSQL
- 아키텍처
- Database
- 컨테이너
- java
- 분산시스템
- Today
- Total
NYO_O
Java 25 본문
지난 시간, 우리는 Java 21에서 Virtual Thread가 어떻게 동시성 모델을 바꾸었는지, Record Patterns와 Pattern Matching for switch가 어떻게 언어 표현력을 완성시켰는지를 살펴보았습니다. 그리고 Java 21 글을 마무리하면서 Structured Concurrency와 Scoped Values가 Preview 상태로 남아 있다는 점도 짚었습니다.
2026.05.29 - [BackEnd/Java] - Java 21
Java 21
지난 시간, 우리는 Java 17에서 Records, Sealed Classes, Pattern Matching for instanceof, Text Blocks가 어떤 배경에서 등장했는지 살펴보았습니다. 언어의 표현력을 한 단계 끌어올리는 변화들이었고, 동시에 Java
ddangnyo.tistory.com
Java 25는 2025년 9월에 출시된 LTS 버전입니다. Java 21이 가상 스레드라는 새로운 패러다임의 시작을 알렸다면, Java 25는 그 패러다임을 실무에서 안전하게 다룰 수 있도록 완성시킨 버전입니다.
Java 22부터 24까지 Preview 형태로 다듬어졌던 기능들이 Java 25에서 마침내 정식 기능으로 확정되었습니다. 오늘은 그 변화들을 하나씩 살펴보겠습니다.
Java 25가 등장한 배경
Java 21에서 Virtual Thread가 정식화되면서 스레드를 수십만 개씩 생성하는 것이 현실적으로 가능해졌습니다. 그런데 여기서 새로운 질문이 생겼습니다.
"스레드를 많이 만드는 것은 쉬워졌는데, 그 스레드들의 생명 주기와 오류를 어떻게 안전하게 관리할 것인가?"
수십 개의 Virtual Thread를 동시에 실행하다가 그 중 하나에서 예외가 발생했을 때, 나머지 스레드들을 깔끔하게 취소하고 자원을 회수하는 것은 기존 방식으로는 까다로운 작업이었습니다. ThreadLocal은 수십만 개의 Virtual Thread 환경에서 메모리 부담이 크다는 문제도 여전히 남아 있었습니다.
Java 25는 이 두 가지 미완의 과제를 정식화하는 것을 핵심으로 삼았습니다. 거기에 언어 문법의 추가적인 완성과 JVM 성능 개선이 함께 담겼습니다.
Oracle은 Java 25에 대해 최소 8년간의 장기 지원을 제공할 계획이며, 이번 버전에는 총 18개의 JEP(JDK Enhancement Proposal)가 포함되었습니다.
구조적 동시성 정식화
사전 지식 — Thread Leak이란? 생성된 스레드가 작업이 끝난 후에도 메모리에서 해제되지 않고 계속 남아 자원을 낭비하는 현상입니다. 스레드 수가 많아질수록 누적되는 문제가 커집니다.
Structured Concurrency는 Java 21에서 처음 Preview로 등장하여, Java 25에서 마침내 정식(Standard) 기능이 되었습니다.
이 기능이 해결하고자 한 문제를 코드로 살펴보겠습니다.
// 기존 방식 — 하나가 실패해도 나머지가 계속 실행됨
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<String> user = executor.submit(() -> fetchUser(userId));
Future<String> order = executor.submit(() -> fetchOrder(orderId));
// fetchUser에서 예외 발생 시, fetchOrder는 계속 실행 중
String u = user.get();
String o = order.get();
fetchUser에서 예외가 발생해도 fetchOrder는 백그라운드에서 계속 실행됩니다. 명시적으로 취소 로직을 작성하지 않으면 스레드가 좀비처럼 남아 자원을 낭비합니다.
Structured Concurrency는 이 문제를 구조적으로 해결합니다.
// Java 25 — Structured Concurrency 정식화
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Supplier<String> user = scope.fork(() -> fetchUser(userId));
Supplier<String> order = scope.fork(() -> fetchOrder(orderId));
scope.join(); // 두 작업이 모두 완료될 때까지 대기
scope.throwIfFailed(); // 하나라도 실패하면 예외 전파
return new Response(user.get(), order.get());
}
// try 블록을 벗어나는 순간 모든 자식 스레드가 정리됨
ShutdownOnFailure는 하나의 작업이 실패하면 나머지 형제 스레드들을 즉시 취소합니다. try-with-resources 구문 덕분에 블록을 벗어나는 순간 모든 자식 스레드의 자원이 자동으로 해제됩니다. Thread Leak이 구조적으로 불가능해지는 것입니다.
ShutdownOnSuccess를 사용하면 반대로 가장 먼저 성공한 결과를 반환하고 나머지를 취소하는 패턴도 구현할 수 있습니다.
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
scope.fork(() -> fetchFromPrimaryServer());
scope.fork(() -> fetchFromBackupServer());
scope.join();
return scope.result(); // 먼저 응답한 서버의 결과 반환
}
Virtual Thread가 '빠른 자동차'라면, Structured Concurrency는 그 자동차를 안전하게 제어하는 '고성능 브레이크' 역할을 하게 된 셈입니다. Java 21에서 시작된 동시성 혁신이 Java 25에서 비로소 실무에서 안전하게 쓸 수 있는 형태로 완성되었습니다.
Scoped Values 정식화
Scoped Values 역시 Java 21에서 Preview로 등장하여 Java 25에서 정식화되었습니다. (JEP 506)
사전 지식 — ThreadLocal이란? 스레드마다 독립적인 값을 저장할 수 있는 Java의 내장 기능입니다. 로그인한 사용자 정보나 트랜잭션 ID처럼 요청 흐름 전반에 걸쳐 값을 전달할 때 자주 사용합니다. 다만 값을 명시적으로 제거하지 않으면 메모리 누수가 발생할 수 있고, 수만 개의 Virtual Thread 환경에서는 각 스레드마다 ThreadLocal 복사본이 생성되어 메모리 부담이 커집니다.
기존 ThreadLocal은 Virtual Thread 환경에서 두 가지 문제를 가집니다. 값의 변경이 자유로워 데이터를 추적하기 어렵고, 수십만 개의 Virtual Thread가 각각 ThreadLocal을 할당받으면 메모리 오버헤드가 상당합니다.
ScopedValue는 이 문제를 불변성과 범위 제한으로 해결합니다.
// ScopedValue 선언
static final ScopedValue<String> USER_ID = ScopedValue.newInstance();
void handleRequest(String userId) {
// 특정 범위 안에서만 값을 바인딩
ScopedValue.where(USER_ID, userId).run(() -> {
processOrder();
sendNotification();
});
// 범위를 벗어나면 자동으로 해제 — 명시적 remove 불필요
}
void processOrder() {
String userId = USER_ID.get(); // 범위 안이라면 어디서든 접근 가능
}
ThreadLocal과 비교했을 때 차이가 명확합니다.
| 항목 | ThreadLocal | ScopedValue |
| 변경 가능 여부 | 가능 (set/get/remove) | 불가능 (불변) |
| 생명 주기 관리 | 개발자가 직접 remove 호출 | 범위 종료 시 자동 해제 |
| 자식 스레드 전달 | 복사 비용 발생 | 복사 없이 공유 |
| Virtual Thread 환경 | 메모리 오버헤드 큼 | 경량, 효율적 |
불변이기 때문에 데이터 흐름을 추적하기 쉽고, 범위가 끝나면 자동으로 정리되기 때문에 메모리 누수 걱정도 없습니다. 특히 Structured Concurrency와 함께 사용하면 자식 스레드들이 부모 스레드의 ScopedValue를 복사 비용 없이 그대로 공유할 수 있습니다.
앞으로 Spring Security의 인증 컨텍스트나 분산 추적(Trace ID) 같은 로직들이 점진적으로 ThreadLocal에서 ScopedValue로 넘어가게 될 것으로 보입니다.
언어 문법의 완성
이름 없는 변수 (Unnamed Variables) 정식화
Java 25에서 언더스코어(_)를 이름 없는 변수로 공식 사용할 수 있게 되었습니다. 사용하지 않는 변수를 처리할 때 의도를 명확히 드러낼 수 있습니다.
// 기존 방식 — 사용하지 않는 변수에 이름을 붙여야 했음
try {
processOrder();
} catch (Exception e) { // e를 실제로 사용하지 않는데도 이름이 필요
log.error("처리 실패");
}
// Java 25 — 이름 없는 변수
try {
processOrder();
} catch (Exception _) { // 사용하지 않음을 명시
log.error("처리 실패");
}
Lambda에서 사용하지 않는 매개변수에도 동일하게 적용할 수 있습니다.
// 기존 방식
list.forEach(item -> System.out.println("처리 완료"));
// Java 25 — 매개변수를 쓰지 않음을 명시
list.forEach(_ -> System.out.println("처리 완료"));
작은 변화지만, 코드를 읽는 사람에게 "이 변수는 의도적으로 무시한 것"임을 명확히 전달할 수 있어서 가독성이 향상됩니다.
원시 타입 패턴 매칭 (Primitive Types in Pattern Matching, Preview)
기존 Pattern Matching은 참조 타입(Reference Type)에만 사용할 수 있었습니다. Java 25의 Preview 기능으로 int, long, double 같은 원시 타입(Primitive Type)에도 패턴 매칭을 적용할 수 있게 되었습니다.
// 기존 방식 — 원시 타입은 switch에서 값 비교만 가능
switch (value) {
case 1 -> "하나";
case 2 -> "둘";
default -> "기타";
}
// Java 25 Preview — 원시 타입에도 instanceof, 패턴 매칭 적용
if (value instanceof int i && i > 0) {
System.out.println("양의 정수: " + i);
}
Object obj = 42; // int가 Object로 다뤄지는 상황
String result = switch (obj) {
case int i when i < 0 -> "음수";
case int i when i == 0 -> "영";
case int i -> "양수";
default -> "정수 아님";
};
AI 추론 관련 애플리케이션에서 수치 데이터를 다룰 때 특히 유용한 기능입니다. Preview이므로 향후 정식화될 가능성이 높습니다.
모듈 임포트 선언 (Module Import Declarations)
특정 모듈이 내보내는 모든 패키지를 한 줄로 임포트할 수 있게 되었습니다.
// 기존 방식 — 패키지를 하나씩 임포트
import java.util.List;
import java.util.Map;
import java.util.ArrayList;
import java.util.stream.Collectors;
// Java 25 — 모듈 단위로 한 번에 임포트
import module java.base;
초보자가 패키지 계층을 몰라도 라이브러리를 쉽게 사용할 수 있고, 여러 패키지에 걸쳐 있는 API를 사용할 때 임포트 문의 반복을 줄일 수 있습니다.
유연한 생성자 본문 (Flexible Constructor Bodies)
기존에는 생성자에서 super() 또는 this() 호출 이전에 어떠한 코드도 작성할 수 없었습니다. Java 25에서는 슈퍼클래스 생성자 호출 전에 입력값 검증이나 안전한 계산을 먼저 수행할 수 있게 되었습니다.
// 기존 방식 — super() 이전에 검증 불가
class PositiveNumber extends Number {
PositiveNumber(int value) {
super(value); // 검증 없이 먼저 호출해야 했음
if (value <= 0) throw new IllegalArgumentException();
}
}
// Java 25 — super() 이전에 검증 가능
class PositiveNumber extends Number {
PositiveNumber(int value) {
if (value <= 0) throw new IllegalArgumentException(); // 먼저 검증
super(value);
}
}
생성자의 안전성과 표현력이 동시에 향상되는 변화입니다.
성능 개선
컴팩트 객체 헤더 (Compact Object Headers, JEP 519)
Java의 모든 객체는 생성될 때 내부적으로 헤더 정보를 가집니다. 기존에는 64비트 아키텍처에서 객체 헤더가 96~128비트를 차지했는데, Java 25에서 이를 64비트로 줄였습니다.
단순해 보이지만 효과는 상당합니다. 수백만 개의 객체를 다루는 애플리케이션에서 전체 메모리 사용량이 줄어들고, 데이터가 CPU 캐시에 더 잘 올라가는 데이터 로컬리티(Data Locality) 향상 효과도 기대할 수 있습니다.
AOT 컴파일 개선 (JEP 514, 515)
사전 지식 — AOT(Ahead-Of-Time) 컴파일이란? 프로그램을 실행하기 전에 미리 기계어로 컴파일해 두는 방식입니다. JVM은 기본적으로 실행 중에 JIT(Just-In-Time) 컴파일로 최적화하는데, 이 과정에서 초기 구동 시간이 느려지는 문제가 있었습니다. AOT는 이 워밍업 시간을 줄이기 위해 도입되었습니다.
Java 25에서 AOT 관련 기능이 두 가지 개선되었습니다.
사전 명령줄 인체공학은 AOT 캐시를 생성하는 명령어를 단순화하여, 복잡한 옵션 없이도 AOT의 이점을 누릴 수 있게 했습니다.
사전 기법 프로파일링은 애플리케이션을 실제 프로덕션에서 실행하기 전에 훈련 실행(Training Run)을 통해 JIT 프로파일을 미리 수집하고, 그 정보를 AOT 캐시에 담아 배포합니다. 덕분에 프로덕션 환경에서 JIT 컴파일러가 프로파일 수집을 기다리지 않고 처음부터 최적화된 네이티브 코드를 실행할 수 있습니다. 서버리스나 컨테이너 환경처럼 빠른 시작이 중요한 환경에서 특히 체감 효과가 클 것으로 보입니다.
그 외 주목할 변화들
보안 강화
키 파생 함수 API가 도입되었습니다. 비밀 키와 데이터로부터 추가 키를 유도하는 암호학적 알고리즘을 위한 표준 API로, 양자 컴퓨팅 환경에 대비한 포스트 양자 암호화(PQC, Post-Quantum Cryptography)로의 전환을 준비하는 기반이 됩니다.
PEM 인코딩 API(Preview)도 추가되었습니다. 암호화 키, 인증서 등을 널리 사용되는 PEM 형식으로 인코딩하고 디코딩하는 API로, 보안 인증 시스템과의 통합이 더 쉬워졌습니다.
모니터링 개선
JDK Flight Recorder(JFR)에 세 가지 개선이 이루어졌습니다. CPU 시간 프로파일링으로 더 정확한 성능 분석이 가능해지고, 협동 샘플링으로 스택 트레이스 수집의 안정성이 높아졌습니다. 메서드 타이밍 및 추적(JEP 520)은 바이트코드 계측을 통해 성능 병목 지점을 더 세밀하게 파악할 수 있게 해줍니다.
압축 소스 파일 및 인스턴스 메인 메서드
Java 21에서 Preview로 도입되었던 기능이 Java 25에서 정식화되었습니다. public static void main(String[] args) 없이도 실행할 수 있는 간결한 진입점 방식이 공식 지원됩니다.
// Java 25 정식화 — 클래스 선언 없이 바로 실행 가능
void main() {
System.out.println("Hello, Java 25!");
}
학습용이나 스크립트성 코드 작성 시 진입장벽을 낮추는 데 기여합니다.
Java 25, 언제 도입하면 좋을까
Java 25는 분명 매력적인 버전입니다. 그렇다고 당장 운영 서버를 Java 25로 올려야 할까요? 실무적인 관점에서는 단계적인 접근이 합리적입니다.
현재 Java 17을 사용 중이라면 Java 21로의 마이그레이션을 먼저 고려하는 것이 좋습니다. Java 21은 이미 생태계가 충분히 안정화되어 있고, Spring Boot 3.x와의 호환성도 검증되어 있습니다.
이미 Java 21을 안정적으로 운영 중이라면 개발 환경에서 Java 25의 Structured Concurrency와 Scoped Values를 선제적으로 테스트해 보는 것을 권장합니다. Spring Boot 등 주요 프레임워크의 Java 25 공식 지원이 안정화되는 시점에 맞춰 전환을 준비하는 전략이 가장 합리적일 것입니다.
마무리
오늘은 Java 25의 주요 변화를 살펴보았습니다. Structured Concurrency와 Scoped Values의 정식화는 Java 21에서 열어젖힌 가상 스레드 생태계를 실무에서 안전하게 다룰 수 있도록 내실을 다진 변화입니다. 여기에 컴팩트 객체 헤더, AOT 프로파일링 개선, 양자 내성 암호화 준비까지 더해졌습니다.
Java 8부터 Java 25까지 이 시리즈를 통해 살펴보았듯, 각 LTS 버전은 그 시대의 요구에 응답하며 언어를 진화시켜 왔습니다. Java 8이 함수형 패러다임을 열었고, Java 17이 언어 표현력을 높였으며, Java 21이 동시성 모델을 바꾸었다면, Java 25는 그 모든 변화를 실무에서 안전하게 쓸 수 있는 형태로 마무리했습니다.
'BackEnd > Java' 카테고리의 다른 글
| GC, 자바는 메모리를 어떻게 스스로 정리할까 (0) | 2026.05.29 |
|---|---|
| JVM 메모리 구조 — Heap, Stack (0) | 2026.05.29 |
| Java 21 (1) | 2026.05.29 |
| Java 17 (0) | 2026.05.29 |
| Java 11 (0) | 2026.05.29 |