안녕하세요
오늘은 자바 생태계에서 가장 뜨거운 감자이자, 많은 기업들이 도입을 서두르고 있는 JDK 21(Java Development Kit 21)에 대해 아주 깊이 있게 파헤쳐 보려고 합니다.
아마 많은 분들이 현재 JDK 8이나 JDK 11, 혹은 가장 최근의 장기 지원 버전(LTS)이었던 JDK 17을 사용하고 계실 텐데요.
2023년 9월에 출시된 JDK 21은 단순한 버전업을 넘어, 자바 역사상 가장 혁신적인 기능 중 하나인 가상 스레드(Virtual Threads)가 정식으로 포함된 기념비적인 LTS 버전입니다.
기존 JDK 17 대비 어떤 점이 획기적으로 좋아졌는지, 그리고 실제 코드 레벨에서는 무엇이 달라졌는지 상세하게 분석해 드리겠습니다.
https://www.oracle.com/java/technologies/javase/21-relnote-issues.html
JDK 21 Release Notes, Important Changes, and Information
These notes describe important changes, enhancements, removed APIs and features, deprecated APIs and features, and other information about JDK 21 and Java SE 21. In some cases, the descriptions provide links to additional detailed information about an issu
www.oracle.com
1. 자바의 새로운 기준, JDK 21이 가져온 거대한 변화의 물결
자바는 그동안 꾸준히 발전해 왔습니다. JDK 8에서 람다와 스트림이 도입되며 함수형 프로그래밍의 문을 열었고, JDK 17에서는 레코드(Record)와 봉인된 클래스(Sealed Classes) 등을 통해 모던 언어로서의 입지를 다졌죠. 하지만 JDK 21은 '동시성(Concurrency)'과 '생산성(Productivity)'이라는 두 마리 토끼를 완벽하게 잡은 버전으로 평가받습니다.
기존의 자바가 가진 한계를 뛰어넘는 기술적 도약이 포함되어 있기 때문인데요. 특히 서버 사이드 개발자라면 반드시 알아야 할 내용들이 가득합니다. 이제부터 핵심적인 변경 사항들을 하나씩 뜯어보며, 여러분의 코드를 어떻게 더 우아하고 효율적으로 바꿀 수 있을지 알아보겠습니다.
2. JDK 21의 핵심 변경 기능 및 문법 심층 분석
JDK 21에는 수많은 JEP(Java Enhancement Proposals)가 포함되어 있지만, 그중에서도 개발자가 피부로 느낄 수 있는 가장 중요한 변화들을 엄선했습니다.
2-1) 가상 스레드 (Virtual Threads) - 자바 동시성의 혁명
가장 먼저 소개할 기능은 단연 가상 스레드(Project Loom)입니다. 이 기능 하나만으로도 JDK 21로 넘어갈 이유는 충분하다고 해도 과언이 아닙니다.
2-1-1) 설명: 왜 가상 스레드인가?
기존 자바의 스레드(Platform Thread)는 운영체제(OS)의 스레드와 1:1로 매핑되는 구조였습니다. OS 스레드는 생성 비용이 비싸고 개수 제한이 있어, 수천, 수만 개의 동시 요청을 처리하려면 복잡한 비동기 프로그래밍(Reactive Programming)을 사용해야만 했죠. 하지만 비동기 코드는 작성하기도 어렵고 디버깅은 더더욱 어려웠습니다.
JDK 21의 가상 스레드는 JVM 내부에서 관리되는 경량 스레드입니다. OS 스레드 하나 위에서 수많은 가상 스레드가 번갈아 가며 실행될 수 있습니다. 덕분에 기존의 단순한 동기(Blocking) 방식의 코드를 그대로 작성하면서도, 비동기 프로그래밍 수준의(혹은 그 이상의) 처리량을 낼 수 있게 되었습니다. 이제 스레드 풀(Thread Pool)을 관리하는 고통에서 해방될 수 있습니다!
2-1-2) 코드 예시 및 결과값
기존 방식과 가상 스레드 방식을 비교해 보겠습니다. 10,000개의 스레드를 생성하여 1초간 대기하는 작업을 수행한다고 가정해 봅시다.
Java
import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;
public class VirtualThreadExample {
public static void main(String args) {
long start = System.currentTimeMillis();
// JDK 21에서 도입된 가상 스레드 실행자 사용
// try-with-resources 구문을 사용하여 자동 종료 처리
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// 10,000개의 작업을 가상 스레드로 동시에 실행
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
try {
// 스레드를 1초간 잠재움 (Blocking I/O 시뮬레이션)
Thread.sleep(Duration.ofSeconds(1));
return i;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
});
} // 여기서 모든 가상 스레드가 종료될 때까지 기다립니다 (Structured Concurrency)
long end = System.currentTimeMillis();
System.out.println("총 소요 시간: " + (end - start) + "ms");
}
}
/*
[코드 실행 결과값]
총 소요 시간: 1050ms
(주석: 1만 개의 스레드를 생성했음에도 불구하고, 거의 1초 남짓한 시간에 완료됩니다.
기존 플랫폼 스레드였다면 스레드 생성 비용과 컨텍스트 스위칭 오버헤드로 인해 훨씬 오래 걸리거나
OutOfMemoryError가 발생했을 수 있습니다.)
*/
이처럼 가상 스레드는 I/O 작업이 많은 웹 서버 환경에서 하드웨어 자원 효율을 극대화하여 처리량을 비약적으로 높여줍니다.
2-2) 시퀀스 컬렉션 (Sequenced Collections) - 순서가 있는 데이터 처리를 더 쉽게
자바의 컬렉션 프레임워크는 강력하지만, '순서가 있는 데이터'를 다룰 때 일관성이 부족했습니다. 예를 들어 List는 get(0)으로 첫 요소를 가져오지만, Deque는 getFirst(), SortedSet은 first()를 사용하는 등 API가 파편화되어 있었죠.
2-2-1) 설명: 통일된 접근 방식
JDK 21에서는 SequencedCollection, SequencedSet, SequencedMap이라는 새로운 인터페이스를 도입하여 컬렉션 계층 구조를 재정비했습니다. 이제 어떤 컬렉션을 사용하든 첫 번째 요소와 마지막 요소를 조회하거나 추가/삭제하는 방법이 통일되었습니다. 또한, reversed() 메서드를 통해 원본 데이터를 건드리지 않고도 역순 뷰(View)를 손쉽게 얻을 수 있게 되었습니다.
2-2-2) 코드 예시 및 결과값
Java
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.SequencedCollection;
import java.util.SequencedMap;
public class SequencedCollectionExample {
public static void main(String args) {
// 1. List (SequencedCollection 구현체)
SequencedCollection<String> list = new ArrayList<>();
list.add("Java");
list.add("Kotlin");
list.add("Python");
// JDK 21의 새로운 메서드들
list.addFirst("C++"); // 맨 앞에 추가
list.addLast("Go"); // 맨 뒤에 추가
System.out.println("전체 리스트: " + list);
System.out.println("첫 번째 요소: " + list.getFirst());
System.out.println("마지막 요소: " + list.getLast());
// 2. Map (SequencedMap 구현체) - LinkedHashMap 등 순서가 있는 맵
SequencedMap<String, Integer> map = new LinkedHashMap<>();
map.put("Apple", 1000);
map.put("Banana", 2000);
map.putFirst("Orange", 1500); // 맨 앞에 엔트리 추가
map.putLast("Grape", 3000); // 맨 뒤에 엔트리 추가
System.out.println("전체 맵: " + map);
System.out.println("첫 번째 키: " + map.firstEntry().getKey());
System.out.println("역순 맵 뷰: " + map.reversed());
}
}
/*
[코드 실행 결과값]
전체 리스트: [C++, Java, Kotlin, Python, Go]
첫 번째 요소: C++
마지막 요소: Go
전체 맵: {Orange=1500, Apple=1000, Banana=2000, Grape=3000}
첫 번째 키: Orange
역순 맵 뷰: {Grape=3000, Banana=2000, Apple=1000, Orange=1500}
*/
이제 list.get(list.size() - 1) 처럼 장황한 코드를 작성할 필요가 없습니다. list.getLast() 하나면 충분합니다!
2-3) 레코드 패턴 (Record Patterns) - 데이터 분해의 미학
JDK 14에서 도입된 record는 불변 데이터 객체를 만드는 데 아주 유용했습니다. JDK 21에서는 이 record를 더욱 강력하게 사용할 수 있도록 레코드 패턴이 정식 기능으로 추가되었습니다.
2-3-1) 설명: instanceof와 패턴 매칭의 결합
기존에는 객체의 타입 확인(instanceof) 후, 내부에 있는 데이터를 꺼내 쓰려면 명시적인 형 변환(Casting)과 접근자 메서드(Getter) 호출이 필요했습니다. 하지만 레코드 패턴을 사용하면 타입 확인과 동시에 내부 필드 값을 바로 변수로 추출(Deconstruction) 할 수 있습니다. 코드가 훨씬 간결하고 직관적으로 변합니다.
2-3-2) 코드 예시 및 결과값
Java
public class RecordPatternExample {
// Record 정의
record Point(int x, int y) {}
record ColoredPoint(Point p, String color) {}
public static void printPoint(Object obj) {
// JDK 17 스타일 (여전히 조금 번거로움)
if (obj instanceof Point p) {
System.out.println("JDK 17: x=" + p.x() + ", y=" + p.y());
}
// JDK 21 스타일: 레코드 패턴 매칭
// Point 내부의 x, y를 바로 변수로 추출
if (obj instanceof Point(int x, int y)) {
System.out.println("JDK 21: x=" + x + ", y=" + y);
}
}
// 중첩된 레코드 패턴 (Nested Record Patterns)
public static void printColoredPoint(Object obj) {
// ColoredPoint 안에 있는 Point의 x, y까지 한 번에 추출!
if (obj instanceof ColoredPoint(Point(int x, int y), String color)) {
System.out.println("Color: " + color + ", Position: " + x + "," + y);
}
}
public static void main(String args) {
Point point = new Point(10, 20);
ColoredPoint coloredPoint = new ColoredPoint(point, "Red");
printPoint(point);
printColoredPoint(coloredPoint);
}
}
/*
[코드 실행 결과값]
JDK 17: x=10, y=20
JDK 21: x=10, y=20
Color: Red, Position: 10,20
*/
이 기능은 복잡한 데이터 구조를 다룰 때, 특히 JSON 파싱 결과를 처리하거나 계층적인 DTO를 다룰 때 엄청난 생산성 향상을 가져옵니다.
2-4) Switch 패턴 매칭 (Pattern Matching for Switch) - if-else의 종말을 고하다
switch 문이 단순히 정수나 문자열, Enum만 비교하던 시절은 지났습니다. JDK 21에서는 switch 문에서 패턴 매칭을 사용할 수 있게 되었습니다.
2-4-1) 설명: 타입 검사와 조건 로직의 통합
이제 switch의 case 레이블에 타입을 지정할 수 있으며, when 키워드를 사용해 추가적인 조건(Guard Clause)까지 붙일 수 있습니다. 이를 통해 복잡하고 지저분한 if-else if 체인을 깔끔하고 가독성 높은 switch 표현식으로 대체할 수 있습니다. 또한 null 케이스도 switch 내부에서 직접 처리가 가능해졌습니다.
2-4-2) 코드 예시 및 결과값
Java
public class SwitchPatternExample {
public static String formatterPatternSwitch(Object obj) {
return switch (obj) {
case Integer i -> String.format("정수: %d", i);
case Long l -> String.format("Long 타입: %d", l);
case Double d -> String.format("실수: %f", d);
// when 절을 사용한 추가 조건 (Guarded Pattern)
case String s when s.length() > 5 -> "긴 문자열: " + s;
case String s -> "짧은 문자열: " + s;
// null 처리 가능
case null -> "널 값입니다";
default -> obj.toString();
};
}
public static void main(String args) {
System.out.println(formatterPatternSwitch(10));
System.out.println(formatterPatternSwitch("Hello World"));
System.out.println(formatterPatternSwitch("Hi"));
System.out.println(formatterPatternSwitch(null));
}
}
/*
[코드 실행 결과값]
정수: 10
긴 문자열: Hello World
짧은 문자열: Hi
널 값입니다
*/
코드의 의도가 명확해지고, 컴파일러가 모든 케이스를 커버하는지 체크(Exhaustiveness Check) 해주기 때문에 버그 발생 가능성도 줄어듭니다.
2-5) 세대별 ZGC (Generational ZGC, JEP 439) - 성능의 비약적 향상
가비지 컬렉터(GC)의 성능이 또 한 번 진화했습니다.
2-5-1) 설명
ZGC는 대용량 메모리에서도 짧은 지연 시간(Low Latency)을 보장하는 GC입니다. JDK 21에서는 ZGC에 '세대별(Generational)' 개념이 도입되었습니다. 즉, 금방 사라지는 객체(Young)와 오래 남는 객체(Old)를 구분해서 관리함으로써, CPU 사용량은 줄이고 처리량(Throughput)은 크게 높였습니다.
2-5-2) 사용법
애플리케이션 실행 시 JVM 옵션으로 -XX:+UseZGC -XX:+ZGenerational을 추가하여 활성화할 수 있습니다. (대용량 힙을 쓰는 서버라면 강력 추천!)
2-6) 문자열 템플릿 (String Templates, JEP 430, Preview) - 문자열 조합의 혁신
※ 주의: 이 기능은 JDK 21에서 Preview로 도입되었으나, 이후 JDK 23에서 재설계를 위해 제거(Retracted)되었습니다. JDK 21에서는 사용 가능하지만, 장기적인 사용 시 API 변경 가능성을 염두에 두어야 합니다.
2-6-1) 설명
"Hello " + name 처럼 + 연산자를 쓰거나 String.format을 쓰는 번거로움을 해결합니다. STR 프로세서를 이용해 변수를 문자열 사이에 안전하고 쉽게 넣을 수 있습니다.
2-6-2) 코드 예시
Java
String name = "Java";
int version = 21;
// STR 프로세서 사용 (Preview 기능 활성화 필요)
String message = STR."Welcome to \{name} \{version}!";
System.out.println(message); // Welcome to Java 21!
2-7) 파라미터 없는 main 메서드 등 (Unnamed Classes & Instance Main Methods, JEP 445, Preview) 🐣
자바를 처음 배우는 사람들을 위한 문법적 간소화입니다.
public static void main(String args)... 자바의 진입장벽이었던 이 긴 코드를 줄였습니다. 클래스 선언 없이, static 없이 void main() 만으로도 프로그램을 실행할 수 있게 하는 프리뷰 기능입니다. 교육용이나 간단한 스크립트 작성 시 유용합니다.
2-8) 이름 없는 패턴 및 변수 (Unnamed Patterns and Variables, JEP 443, Preview)
2-8-1) 설명
코드 작성 중 사용하지 않는 변수(예: 예외 처리의 e, 반복문의 인덱스 등)를 굳이 이름 짓지 않고 밑줄(_)로 처리할 수 있습니다. 코드의 의도를 명확히 하고 가독성을 높여줍니다.
2-8-2) 코드 예시
Java
try {
int i = Integer.parseInt("NotNumber");
} catch (NumberFormatException _) { // 변수명 대신 _ 사용
System.out.println("숫자가 아닙니다.");
}
2-9) Scoped Values (JEP 446, Preview) - ThreadLocal의 대안
가상 스레드가 수백만 개 생성될 때, 기존의 ThreadLocal을 사용하면 메모리 사용량이 급증할 수 있습니다. Scoped Values는 데이터를 불변(Immutable)으로 유지하면서 특정 범위(Scope) 내에서만 안전하고 효율적으로 공유할 수 있는 경량화된 대안입니다. 가상 스레드 환경에서 컨텍스트 전파를 위한 최적의 솔루션입니다.
2-10) 기타 유용한 API 추가 (Math.clamp, StringBuilder.repeat)
소소하지만 개발자의 퇴근 시간을 앞당겨줄 유틸리티 메서드들도 추가되었습니다.
- Math.clamp(value, min, max): 값이 최소/최대 범위를 벗어나지 않도록 조정해 줍니다. Math.max(min, Math.min(val, max)) 같은 복잡한 코드를 한 줄로 줄여줍니다.
- StringBuilder.repeat(CharSequence, int): 문자열을 반복해서 붙이는 기능이 StringBuilder에 내장되었습니다.
https://www.geeksforgeeks.org/java/java-jdk-21-new-features-of-java-21/
Java JDK 21: New Features of Java 21 - GeeksforGeeks
Your All-in-One Learning Portal: GeeksforGeeks is a comprehensive educational platform that empowers learners across domains-spanning computer science and programming, school education, upskilling, commerce, software tools, competitive exams, and more.
www.geeksforgeeks.org
3. JDK 8, 11, 17에서 JDK 21로 마이그레이션 시 고려사항
JDK 21은 매력적인 기능으로 무장하고 있지만, 실무 환경에서 버전을 올릴 때는 몇 가지 주의해야 할 점들이 있습니다. 무턱대고 업그레이드했다가 장애를 겪지 않도록 다음 사항들을 꼭 체크해 보세요.
3-1) 공통 고려사항 (모든 버전 해당)
- Lombok 버전 업데이트: 가장 중요합니다! JDK 21은 컴파일러 내부가 변경되어 구버전 롬복과 호환되지 않습니다. 반드시 Lombok 1.18.30 이상으로 업데이트해야 합니다.
- Spring Boot 버전: 가상 스레드를 제대로 쓰려면 Spring Boot 3.2 이상을 권장합니다. (spring.threads.virtual.enabled=true 옵션 하나로 톰캣에서 가상 스레드를 쓸 수 있습니다!)
- UTF-8 기본화 (JEP 400): JDK 18부터 기본 인코딩이 UTF-8로 고정되었습니다. 한글 윈도우(MS949) 환경에서 파일 입출력을 하던 레거시 코드가 있다면 인코딩 설정(-Dfile.encoding=COMPAT)을 확인해야 할 수 있습니다.
3-2) JDK 8 -> JDK 21 (대격변)
이 구간은 점프해야 할 단계가 매우 많습니다.
- Java EE 패키지명 변경: javax.* 패키지가 jakarta.*로 바뀌었습니다(Java 17/Spring Boot 3 전환 시점). 서블릿, JPA, Validation 관련 import 문을 모두 수정해야 합니다.
- 모듈 시스템 (Java 9): 내부 API 접근이 제한될 수 있습니다. jdeps 도구로 의존성을 확인하세요.
- 32비트 지원 중단: JDK 21부터는 윈도우 32비트를 지원하지 않습니다.
3-3) JDK 11 -> JDK 21
- Jakarta EE 전환: JDK 8과 마찬가지로 javax -> jakarta 패키지명 변경 이슈가 가장 큽니다.
- 제거된 API: Thread.stop(), suspend(), resume() 등 오랫동안 Deprecated였던 메서드들이 이제는 정말로 예외를 던지거나 제거되었습니다.
3-4) JDK 17 -> JDK 21
상대적으로 가장 수월한 경로입니다.
- 문법적 호환성: 대부분의 코드는 그대로 동작합니다.
- 가상 스레드 피닝(Pinning): 가상 스레드 도입 시 synchronized 블록을 사용하면 스레드가 고정(Pinning)되어 성능 이점을 못 볼 수 있습니다. ReentrantLock으로 대체하거나, 라이브러리가 최신 버전인지 확인해야 합니다.
- Spring Boot 3.2+: 3.0이나 3.1을 쓰고 있다면 3.2로 올려야 JDK 21의 기능을 100% 활용할 수 있습니다.
3-5) 더 이상 지원되지 않는(Deprecated/Removed) 기능 체크
오래된 버전에서 건너뛰는 경우, 제거된 API나 기능이 있는지 확인해야 합니다. 예를 들어, JDK 21에서는 윈도우 32비트 지원이 완전히 종료되었습니다. jdeprscan 도구를 사용하여 현재 프로젝트가 사용 중인 API 중 향후 문제가 될 수 있는 부분을 미리 점검하는 것이 좋습니다.
https://blogs.halodoc.io/migrating-from-jdk-17-to-jdk-21-an-overview-and-practical-guide/
Migrating from JDK 17 to JDK 21: An Overview and Practical Guide
This blog is an Overview and Practical Guide to Migrating from JDK 17 to JDK 21.
blogs.halodoc.io
4. 마무리하며
JDK 21은 자바 개발자들에게 새로운 무기를 쥐여주는 것과 같습니다. 가상 스레드를 통한 고성능 서버 구축, 패턴 매칭을 통한 코드 간결화, 시퀀스 컬렉션을 통한 편의성 증대 등은 개발의 즐거움을 되찾아 줄 것입니다.
물론 레거시 시스템을 운영 중이라면 마이그레이션이 부담스러울 수 잇지만, JDK 21은 LTS(Long Term Support) 버전으로, 향후 수년간 자바 생태계의 표준이 될 수 있습니다. 때문에 지금부터 조금씩 준비하시는 것도 도움이 될 거 같습니다.

'IT > JAVA&SpringBoot' 카테고리의 다른 글
| [Java 21] 서버 성능을 10배 높이는 비밀: 블로킹 vs 논블로킹, 그리고 가상 스레드의 혁명 (0) | 2025.12.15 |
|---|---|
| [Spring Boot] 백엔드 개발자 필독! HTTP 클라이언트 5대장 완벽 비교 & 실전 코드 예시 모음(Restclient, WebClient, OpenFeign, HttpExchange) (0) | 2025.12.15 |
| 자바 개발자의 연봉을 결정짓는 운명의 시간! JDK 25 & Spring Boot 4 완벽 해부 (0) | 2025.12.10 |
| ☕ Java 개발자의 영원한 숙제, "도대체 어떤 JDK를 써야 할까?" 완벽 정리 (0) | 2025.12.10 |
| MessageSource 기능을 static 메서드로 사용하기 (0) | 2022.03.18 |
댓글