자바
[자바] Java 21 버전 특징과 주요 변경 사항
kyjdummy
2025. 5. 9. 21:56
[ 자바 21의 지원 일자 ]
- 무료 지원 일자 : 2023-09 ~ 2028-09
- 유료지원 일자 : 2031-09
[ 호환 스프링 부트 ]
- Spring Boot 호환 : 3.2.x 이상(3.2가 21을 정식 지원)
- 안정 적인 버전 : 자바 21은 3.2.1이상권장
[ 가비지 컬렉터 ]
- 자바 21에서는 가비지 컬렉터의 성능이 크게 개선되었습니다. 특히 ZGC(Z Garbage Collector)와 G1(Garbage-First) 컬렉터에 많은 최적화가 적용되었습니다.
[ 특징 및 주요 변경 사항 ]
1. 가상 스레드 : 경량 스레드
- 자바 21에서 정식 추가된 가상 스레드는 기존의 플랫폼 스레드(OS 스레드)와 다른 경량 스레드 구현입니다. 전통적인 플랫폼 스레드(OS 스레드)는 생성·컨텍스트 스위칭 비용이 크므로, 대규모 동시성 서버에서 “NIO + 쓰레드풀” 같은 복잡한 비동기를 강제했습니다. 가상 스레드는 JVM 내부 스케줄러가 수천~수백만 개의 스레드를 섬유처럼 가볍게 관리하도록 구현되어, 블로킹 I/O 코드를 ‘그대로’ 고성능으로 실행할 수 있습니다.
- 가상 스레드 특징
- 수백 바이트 수준의 메모리만 사용하여 가볍고, 수백만 개의 가상 스레드 생성 가능하며 기존 스레드 풀과 달리 스레드 생성 비용이 매우 낮아 풀링이 필요 없음
- 가상 스레드는 Thread 클래스의 인스턴스로, 기존 코드와 호환성 유지
- JVM에 의해 관리되며 실제 OS 스레드(캐리어 스레드) 위에서 실행되며, 블로킹 작업 시 다른 가상 스레드로 전환됨
- Executors.newVirtualThreadPerTaskExecutor() 팩토리 메서드로 생성 가능
// 단일 가상 스레드 생성 및 실행
Thread virtualThread = Thread.startVirtualThread(() -> {
System.out.println("가상 스레드에서 실행 중: " + Thread.currentThread());
try {
Thread.sleep(1000); // IO 작업을 시뮬레이션
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 가상 스레드의 완료를 기다림
try {
virtualThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// ExecutorService를 사용한 가상 스레드 풀 생성
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// 10,000개의 동시 작업 실행
List<Future<String>> futures = new ArrayList<>();
for (int i = 0; i < 10_000; i++) {
int taskId = i;
futures.add(executor.submit(() -> {
// 각 가상 스레드에서 실행되는 작업
Thread.sleep(100); // 네트워크 호출 시뮬레이션
return "Task " + taskId + " 완료";
}));
}
// 결과 수집
for (Future<String> future : futures) {
System.out.println(future.get());
}
}
2. 구조적 병행성 (Structured Concurrency)
- 구조적 병행성은 관련된 여러 작업을 하나의 작업 단위로 관리할 수 있게 해주는 병행성 모델입니다. 자바 21에서는 미리 보기 기능으로 제공되고, java.util.concurrent.StructuredTaskScope 클래스를 통해 구현됩니다.
- 구조적 병행성의 필요성
- 관련된 여러 작업의 수명주기를 하나의 블록 안에서 관리 가능
- 에러 처리와 취소가 간단함
- 리소스 누수 가능성이 적음
- 코드의 가독성이 올라감
// 구조적 병행성을 사용하기 위한 미리보기 기능 활성화 필요:
// --enable-preview 옵션 사용
// 사용자 정보를 가져오는 메서드
record User(int id, String name) {}
record Order(int id, String details) {}
User fetchUser(int userId) throws Exception {
Thread.sleep(100); // 네트워크 호출 시뮬레이션
return new User(userId, "사용자_" + userId);
}
List<Order> fetchOrders(int userId) throws Exception {
Thread.sleep(150); // 네트워크 호출 시뮬레이션
return List.of(
new Order(1, "userId=" + userId + ", 상품=A"),
new Order(2, "userId=" + userId + ", 상품=B")
);
}
// 구조적 병행성을 사용한 병렬 데이터 조회
record UserData(User user, List<Order> orders) {}
UserData getUserData(int userId) throws Exception {
// ShutdownOnFailure: 한 작업 실패 시 나머지 모두 취소
// ShutdownOnSuccess: 한 작업 성공 시 나머지 모두 취소
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// 두 작업을 병렬로 시작
StructuredTaskScope.Subtask<User> userTask =
scope.fork(() -> fetchUser(userId));
StructuredTaskScope.Subtask<List<Order>> ordersTask =
scope.fork(() -> fetchOrders(userId));
// 모든 작업이 완료될 때까지 대기
scope.join();
// 실패 확인 및 예외 전파
scope.throwIfFailed();
// 결과 조합
return new UserData(userTask.get(), ordersTask.get());
}
}
3. Sequenced Collection
- 자바 21에서 도입된 새로운 컬렉션 인터페이스로, 명시적인 순서를 가진 컬렉션을 표현합니다. 이 인터페이스들은 컬렉션의 첫 번째와 마지막 요소에 접근하고, 순방향 역방향 순회를 지원하는 메소드를 제공합니다. 기존 자바 컬렉션 API에서는 순서가 있는 컬렉션들(List, Deque 등)이 있었으나 이들 사이에 일관된 인터페이스가 없었습니다.
- 주요 인터페이스
- SequencedCollection: 순서가 있는 컬렉션의 기본 인터페이스
- SequencedSet: 순서가 있는 Set
- SequencedMap: 순서가 있는 Map
- Sequenced Collections는 기존 컬렉션 클래스들(ArrayList, LinkedHashSet, LinkedHashMap 등)의 인터페이스를 확장한 것이므로, 별도의 새 구현을 필요로 하지 않고 기존 코드와 호환됩니다.
// SequencedCollection 사용 예제
List<String> fruits = new ArrayList<>(List.of("사과", "바나나", "오렌지", "포도"));
// 첫 번째와 마지막 요소 접근
String first = fruits.getFirst(); // "사과"
String last = fruits.getLast(); // "포도"
// 역순 컬렉션 얻기
List<String> reversed = fruits.reversed(); // [포도, 오렌지, 바나나, 사과]
// 첫 번째 요소 추가/제거
fruits.addFirst("딸기"); // [딸기, 사과, 바나나, 오렌지, 포도]
String removed = fruits.removeFirst(); // "딸기" 제거, [사과, 바나나, 오렌지, 포도]
// SequencedMap 사용 예제
LinkedHashMap<String, Integer> scores = new LinkedHashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 85);
scores.put("Charlie", 90);
// 첫 번째/마지막 항목 접근
Map.Entry<String, Integer> firstEntry = scores.firstEntry(); // Alice=95
Map.Entry<String, Integer> lastEntry = scores.lastEntry(); // Charlie=90
// 역순 맵 얻기
Map<String, Integer> reversedMap = scores.reversed(); // {Charlie=90, Bob=85, Alice=95}
// 키와 값의 시퀀스 컬렉션 얻기
SequencedSet<String> keys = scores.sequencedKeySet(); // [Alice, Bob, Charlie]
SequencedCollection<Integer> values = scores.sequencedValues(); // [95, 85, 90]
4. 레코드 패턴
- 레코드 패턴은 자바 21에서 정식 추가된 기능으로, 레코드 타입의 구조를 분해하고 매칭할 수 있게 해주는 기능입니다. 패턴 매칭과 함께 사용하면 데이터 처리 코드를 더 간결하고 안전하게 작성할 수 있습니다.
// 중첩 레코드 정의
record Point(int x, int y) {}
record Rectangle(Point topLeft, Point bottomRight) {}
record Circle(Point center, int radius) {}
// 레코드 패턴과 instanceof 사용
void printShape(Object shape) {
if (shape instanceof Rectangle(Point(int x1, int y1), Point(int x2, int y2))) {
int width = x2 - x1;
int height = y2 - y1;
System.out.println("사각형: 너비=" + width + ", 높이=" + height);
}
else if (shape instanceof Circle(Point(int x, int y), int r)) {
System.out.println("원: 중심=(" + x + "," + y + "), 반지름=" + r);
}
}
// switch 문에서의 레코드 패턴
String describeShape(Object shape) {
return switch (shape) {
case Rectangle(Point(var x1, var y1), Point(var x2, var y2)) -> {
int width = x2 - x1;
int height = y2 - y1;
yield "사각형: 너비=" + width + ", 높이=" + height;
}
case Circle(Point(var x, var y), var r) ->
"원: 중심=(" + x + "," + y + "), 반지름=" + r;
default -> "알 수 없는 도형";
};
}
5. 패턴 매칭
- 자바 21에서 패턴 매칭은 정식 추가된 기능으로, switch 문과 표현식에서 패턴 매칭을 사용할 수 있게 해줍니다. 기존 switch 문은 제한적 타입(정소, 열거형, 문자열)만 지원했고, 단순 값 비교만 가능했습니다. 변경 후에는 모든 참조 타입을 switch에서 사용할 수 있고, 타입 확인과 캐스팅이 한 번에 됩니다. 또한, null 값 처리가 자연스러워졌고, 가드 표현식(when)을 통한 조건부 매칭도 가능합니다.
// 객체 타입 패턴 매칭
Object obj = "Hello";
String result = switch (obj) {
case Integer i -> "정수: " + i;
case String s -> "문자열: " + s;
case Double d when d > 0 -> "양의 실수: " + d;
case Double d -> "음의 실수: " + d;
case null -> "null 값";
default -> "기타 타입";
};
System.out.println(result); // "문자열: Hello"
// 레코드 패턴과 결합
sealed interface Shape permits Circle, Rectangle {}
record Circle(Point center, int radius) implements Shape {}
record Rectangle(Point topLeft, Point bottomRight) implements Shape {}
record Point(int x, int y) {}
double calculateArea(Shape shape) {
return switch (shape) {
case Circle(Point(var x, var y), var radius) -> Math.PI * radius * radius;
case Rectangle(Point(var x1, var y1), Point(var x2, var y2)) -> {
int width = x2 - x1;
int height = y2 - y1;
yield (double) width * height;
}
};
}
// null과 when 절 사용
String describe(Object obj) {
return switch (obj) {
case String s when s.length() > 5 -> "긴 문자열";
case String s -> "짧은 문자열";
case Integer i when i > 0 -> "양수";
case Integer i when i < 0 -> "음수";
case Integer i -> "0";
case null -> "null 값";
default -> obj.toString();
};
}
6. 문자열 템플릿(String Templates)
- 문자열 템플릿은 자바 21에서 미리 보기 기능으로 도입된 것으로, 문자열 안에 표현식을 직접 삽일할 수 있게 해주는 기능입니다. 기존에는 문자열 연결 시 (+) 연산자를 통해 연결하고, %s, %d 등을 사용했으나 문자열 템플릿은 이런 문제를 해결해 가독성과 유지 보수성을 높입니다.
// 미리보기 기능 활성화 필요:
// --enable-preview 옵션 사용
// StringTemplate 클래스를 static import
import static java.lang.StringTemplate.STR;
// 기본 사용법
String name = "홍길동";
int age = 30;
String greeting = STR."안녕하세요, \{name}님! 당신은 \{age}세입니다.";
System.out.println(greeting); // "안녕하세요, 홍길동님! 당신은 30세입니다."
// 표현식 사용
int x = 10, y = 20;
String calculation = STR."\{x} + \{y} = \{x + y}";
System.out.println(calculation); // "10 + 20 = 30"
// 멀티라인 템플릿
String html = STR."""
<html>
<body>
<h1>환영합니다, \{name}님!</h1>
<p>현재 나이: \{age}</p>
<p>5년 후 나이: \{age + 5}</p>
</body>
</html>
""";
// 커스텀 포맷터 사용
record Person(String name, int age) {}
List<Person> people = List.of(
new Person("홍길동", 30),
new Person("김철수", 25),
new Person("이영희", 35)
);
// JSON 포맷터 구현 예시
StringTemplate.Processor<String> JSON = template -> {
StringBuilder sb = new StringBuilder();
sb.append("{");
boolean first = true;
for (StringTemplate.Fragment fragment : template.fragments()) {
if (!first) sb.append(",");
first = false;
String key = fragment.value().trim();
if (!key.isEmpty()) {
sb.append("\"").append(key).append("\":");
}
}
sb.append("}");
return sb.toString();
};
// 커스텀 포맷터 사용
String json = JSON."name:\{person.name()}age:\{person.age()}";
7. 키-값 스토어 API 기능
- 자바 21에서 미리 보기 기능으로 도입된 것으로, 간단한 키-값 데이터를 효율적으로 저장하고 관리할 수 있는 API입니다. 기존에는 자바에서 키-값 데이터를 영구적으로 저장하기 위해 외부 데이터베이스나 서드파티 라이브러리를 사용해야 했습니다. 이제는 표준화된 인터페이스 제공으로 인해, 간단한 키-값 데이터를 위한 내장 솔루션을 제공합니다. 이 API는 프리뷰 기능이므로, 실제 사용 시에는--enable-preview 옵션을 지정해야 합니다.
// 프리뷰 기능 활성화 필요:
// --enable-preview 옵션 사용
import java.lang.runtime.KeyValueStore;
import java.lang.runtime.KeyValueStores;
// 키-값 스토어 생성
Path storePath = Path.of("/path/to/store");
try (KeyValueStore store = KeyValueStores.newKeyValueStore(storePath)) {
// 값 저장
store.put("user.1.name", "홍길동");
store.put("user.1.email", "hong@example.com");
store.put("user.1.age", "30");
store.put("user.2.name", "김철수");
store.put("user.2.email", "kim@example.com");
store.put("user.2.age", "25");
// 값 조회
String name = store.get("user.1.name");
System.out.println("사용자 이름: " + name); // "홍길동"
// 키 존재 여부 확인
boolean exists = store.containsKey("user.1.phone");
System.out.println("전화번호 존재: " + exists); // false
// 특정 패턴의 모든 키 조회
List<String> userKeys = store.keys("user.1.*");
System.out.println("사용자1 키: " + userKeys); // [user.1.name, user.1.email, user.1.age]
// 값 삭제
store.remove("user.2.email");
// 트랜잭션 사용
store.transaction(() -> {
store.put("counter", "1");
String counter = store.get("counter");
store.put("counter", String.valueOf(Integer.parseInt(counter) + 1));
// 트랜잭션 내에서 예외 발생 시 롤백
if (someCondition()) {
throw new RuntimeException("트랜잭션 롤백");
}
});
}