
30살을 맞이한 JAVA의 준비
Oracle이 Java24(Oracle JDK 24)를 그저께, 2025년 3월 18일에 정식 출시 발표를 했다.
Java21 나와서 가상스레드 적용된 게 불과 1년 전이었는데 벌써 24다.
Java 24에 대한 기능은 (시차가 있지만) 2025년 3월 18일부터 20일까지 열리는 JavaOne 2025 컨퍼런스에서 공개되고 있다. 이 컨퍼런스의 발표내용에 따르면
"올해로 Java 30주년을 맞았어!"
몰랐다. Java 아직 30살 밖에 안되었구나, 나랑 크게 차이 안 나네 👓 하고 글쿤글쿤 한 번.
"AI 기반 애플리케이션 개발을 지원하려고 확장 중이야!"
AI를 지원한다는 건 어떤 의미인지 궁금 한 번. 조금 더 들어보면
"Java 24에는 20개 이상의 새로운 기능이 있고, 특히 AI와 양자에 대한 암호화 기능을 지원할 거야."
양자 암호화까지 다루려는 큰 그림이 있었다. 이같은 업데이트된 세부 Feature 정보들은 공식 홈페이지에 기재되어있다. JEP를 머릿말로 하여 주요 변경사항이 정리되어 있으니 참고하면 좋을 것 같다.
Oracle Releases Java 24
Oracle today announced the availability of Java 24, the latest version of the world’s number one programming language and development platform.
www.oracle.com
업계는 Java24의 어떤 부분에 주목할까
Java24에 대해 업계에서는 다음과 같은 반응이 있었다.
- Frank Greco (NYJavaSIG 회장): "Java 24의 벡터 API 개선이 AI 애플리케이션 개발에 큰 도움이 될 것 같다."
- Richard Fichtner (XDEV Software GmbH CEO): "Stream Gatherers 기능 덕분에 복잡한 데이터 변환을 더욱 쉽게 처리할 수 있다."
- Dr. Venkat Subramaniam (Agile Developer, Inc. 설립자): "Scoped Values와 Structured Concurrency 같은 기능이 특히 마음에 든다."
- JetBrains (IntelliJ IDEA 개발사): "Java 24를 출시와 동시에 지원할 예정이며, 최신 Java 기능을 쉽게 사용할 수 있도록 제공할 것이다."
언급된 기술에 대해 좀 더 자세히 살펴보자.
Stream Gatheres? 더 직관적인 데이터 변환 방식
기존 Java의 Stream API는 보통 잘 알고 있는 map(), filter(), collect()와 같은 연산이 존재하고 이를 통해 데이터 변환 작업을 해왔었다. 다만 중간 연산(intermediate operation)을 자유롭게 정의하기가 어려운 한계가 있었다.
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class FlatMapMapExample {
public static void main(String[] args) {
List<Map<String, Object>> jsonData = List.of(
Map.of("id", 1, "name", "Alice"),
Map.of("id", 2, "age", 25), // "name" 없음
Map.of("id", 3, "name", "Bob")
);
List<String> names = jsonData.stream()
.map(map -> map.get("name")) // "name" 필드 가져오기 (Object 타입 반환)
.flatMap(obj -> obj instanceof String ? Stream.of((String) obj) : Stream.empty()) // null 값 제거 후 스트림으로 변환
.collect(Collectors.toList());
System.out.println(names); // [Alice, Bob]
}
}
위는 예를 들기 위해 만든 코드인데, JSON을 받았을 때 "name"을 추출하는 기존방식의 로직이다. 정리하자면 아래와 같다.
- Object를 String으로 변환하는 과정 존재
- map()과 flatMap()이 중첩되며 가독성 저하
- null 값에 대한 별도 처리로 인한 복잡성 증가
여기서 gather() 방식을 적용해보자.
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import java.util.stream.StreamGatherers;
public class StreamGatherersExample {
public static void main(String[] args) {
List<Map<String, Object>> jsonData = List.of(
Map.of("id", 1, "name", "Alice"),
Map.of("id", 2, "age", 25), // "name" 없음
Map.of("id", 3, "name", "Bob")
);
var names = jsonData.stream()
.gather(StreamGatherers.filtering(map -> map.containsKey("name"), // "name"이 있는 경우만 필터링
StreamGatherers.mapping(map -> (String) map.get("name")) // 안전하게 값 변환
))
.toList();
System.out.println(names); // [Alice, Bob]
}
}
- gather()를 통해 중간 연산을 더 직관적으로 정의
- filtering()과 mapping()을 활용한 null처리의 자동화
- 코드의 간결성 향상과 유지보수 편이 기대 가능
이처럼 언급된 Stream Gatherers를 사용하면 더 유연한 중간 연산을 추가할 수 있게되어 복잡한 데이터 변환을 보다 유용하게 처리할 수 있게 되었다. 처리로직이 복잡할수록 해당 과정이 얼마나 눈에 편해진 것인지 와닿을 것이다.
즉, 데이터 변환 로직을 더욱 직관적으로 작성할 수 있어서 좋아! 😶 라는 얘기다.
Scoped Values, Structured Concurrency? 멀티스레드의 개선
Scoped Values라고 하면 스레드 간 데이터를 안전하게 공유하는 기능이고, Structured Concurrency는 멀티스레드 코드의 가독성과 안정성을 높이는 API다.
기존 방식이 아래와 같이 이루어졌다고 치자.
import java.util.concurrent.*;
public class ExecutorExample {
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<String> future1 = executor.submit(() -> {
Thread.sleep(1000);
return "Task 1 Done";
});
Future<String> future2 = executor.submit(() -> {
Thread.sleep(2000);
return "Task 2 Done";
});
try {
System.out.println(future1.get(2500, TimeUnit.MILLISECONDS)); // 2.5초 기다림
System.out.println(future2.get(2500, TimeUnit.MILLISECONDS));
} catch (TimeoutException e) {
System.out.println("Timeout! Cancelling tasks...");
future1.cancel(true); // 작업 취소
future2.cancel(true);
} catch (Exception e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
}
이 코드의 문제점은 이와 같다.
- 개별 future.get() 호출로 인한 코드의 길이 증가
- future.cancel(true)를 사용하더라도 하위 작업의 안전한 취소를 기대하기 어려움
더 설명하자면, 이 코드에선 각 작업이 개별적으로 수행되고 이를 취소하는 것 또한 개별로 이루어진다. 하나의 작업이 실패해도 다른 작업들은 그대로 진행될 수 있는 것이다. 그러니 예외가 발생한다면 하위 작업을 안전하게 취소하기 어려운 상태라 말할 수 있다.
.
이제 Java 24의 방식으로 개선시켜보자
import java.util.concurrent.*;
public class StructuredConcurrencyExample {
public static void main(String[] args) throws InterruptedException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> future1 = scope.fork(() -> {
Thread.sleep(1000);
return "Task 1 Done";
});
Future<String> future2 = scope.fork(() -> {
Thread.sleep(2000);
return "Task 2 Done";
});
scope.join(); // 모든 작업이 끝날 때까지 기다림
scope.throwIfFailed(); // 어떤 작업이 실패하면 전체 취소
System.out.println(future1.resultNow());
System.out.println(future2.resultNow());
}
}
}
StructuredTaskScop는 여러 개의 하위 작업을 하나의 논리적 그룹(Task Scope) 안에서 실행시킨다. 따라서 한 Scope 내이 있는 작업들은 함께 수행되고 함께 종료될 예정이다. 코드가 훨씬 짧고 직관적이며, scope.throwIfFailed()의 경우 하나의 작업이 실패한다면 이 묶음 단위가 자동으로 전체 취소될 수 있게 지원해준다.
이러니 멀티스레드 프로그래밍은 단순화되고 버그를 줄일 수 있을 것 같아! 😶 라는 말이다.
Vector API? AI와 데이터 처리 성능의 개선
사전적으로, Vetor API는 CPU의 SIMD라고해서 Single Instructin Multiple Data 명령어를 활용하는 기능이였다. 즉 여러 개의 숫자를 한 번에 계산하는 방식인 건데, 이 벡터 연산은 AI, 머신러닝, 이미지/영상 처리에서 필수적이다.
기존 자바에서는 이 Vector 연산을 최적화하기가 어려웠지만, 개선된 사항으로 CPu의 벡터 연산을 직접 활용하여 속도를 높이고 AI 및 데이터 처리 성능을 크게 개선 시킨다고 한다. 사례를 봐야 얼마나 개선되었는 지 알 수 있을 것 같다.
끗