diff --git a/docs-taxonomy.yml b/docs-taxonomy.yml
index 9d09b57d..b07bd012 100644
--- a/docs-taxonomy.yml
+++ b/docs-taxonomy.yml
@@ -121,6 +121,7 @@ verification_statuses: [verified, needs-review]
required_source_host: learn.microsoft.com
official_source_hosts:
- learn.microsoft.com
+ - openjdk.org
- kubernetes.io
- www.cncf.io
- cncf.io
diff --git a/docs/services/application-development/java-lts-version-selection/index.md b/docs/services/application-development/java-lts-version-selection/index.md
new file mode 100644
index 00000000..593a1d24
--- /dev/null
+++ b/docs/services/application-development/java-lts-version-selection/index.md
@@ -0,0 +1,430 @@
+---
+services:
+- application-development
+official_sources:
+- title: Support roadmap for the Microsoft Build of OpenJDK
+ url: https://learn.microsoft.com/en-us/java/openjdk/support
+- title: JDK 8 Project
+ url: https://openjdk.org/projects/jdk8/
+- title: JDK 8 Features
+ url: https://openjdk.org/projects/jdk8/features
+- title: JDK 17 Project
+ url: https://openjdk.org/projects/jdk/17/
+- title: JEPs in JDK 17 integrated since JDK 11
+ url: https://openjdk.org/projects/jdk/17/jeps-since-jdk-11
+- title: JEP 248 - Make G1 the Default Garbage Collector
+ url: https://openjdk.org/jeps/248
+- title: JEP 261 - Module System
+ url: https://openjdk.org/jeps/261
+- title: JEP 286 - Local-Variable Type Inference
+ url: https://openjdk.org/jeps/286
+- title: JEP 321 - HTTP Client API
+ url: https://openjdk.org/jeps/321
+- title: JDK 21 Project
+ url: https://openjdk.org/projects/jdk/21/
+- title: JEPs in JDK 21 integrated since JDK 17
+ url: https://openjdk.org/projects/jdk/21/jeps-since-jdk-17
+- title: JDK 25 Project
+ url: https://openjdk.org/projects/jdk/25/
+- title: JEPs in JDK 25 integrated since JDK 21
+ url: https://openjdk.org/projects/jdk/25/jeps-since-jdk-21
+- title: JEP 444 - Virtual Threads
+ url: https://openjdk.org/jeps/444
+- title: JEP 439 - Generational ZGC
+ url: https://openjdk.org/jeps/439
+- title: JEP 491 - Synchronize Virtual Threads without Pinning
+ url: https://openjdk.org/jeps/491
+- title: JEP 483 - Ahead-of-Time Class Loading and Linking
+ url: https://openjdk.org/jeps/483
+- title: JEP 515 - Ahead-of-Time Method Profiling
+ url: https://openjdk.org/jeps/515
+- title: JEP 506 - Scoped Values
+ url: https://openjdk.org/jeps/506
+- title: JEP 519 - Compact Object Headers
+ url: https://openjdk.org/jeps/519
+- title: JEP 521 - Generational Shenandoah
+ url: https://openjdk.org/jeps/521
+document_type: guide
+status: current
+verification_status: verified
+sources_checked_at: 2026-09-16
+last_verified: 2026-09-16
+review_cycle_days: 180
+applies_to:
+- Java SE 8, 17, 21, 25
+title: Java 8·17·21·25 LTS 선택과 현대화 가이드
+description: Java 주요 LTS의 기능과 성능 특성을 비교하고 신규 도입 및 업그레이드 기준과 구현 패턴을 정리합니다.
+technologies:
+- java
+tags:
+- evaluate
+- migrate
+- optimize
+---
+
+# Java 8·17·21·25 LTS 선택과 현대화 가이드
+
+Java 버전 선택은 새 문법의 개수보다 **전체 애플리케이션 스택의 지원
+범위**와 **실제 워크로드에서 측정한 결과**로 결정해야 한다. 이 글은 Java
+8, 17, 21, 25를 비교하지만, 모든 JEP를 나열하기보다 설계와 운영에 영향을
+주는 변화에 집중한다.
+
+## 먼저 내릴 결론
+
+| 상황 | 기본 선택 | 이유 |
+|---|---|---|
+| 프레임워크, 라이브러리, Java agent, base image와 운영 플랫폼이 모두 지원하는 신규 서비스 | **Java 25** | 25까지 누적된 기능과 런타임 개선을 사용하고 다음 업그레이드까지의 여유를 확보하기 쉽다. |
+| Java 25 지원이 아직 완전하지 않은 기존 서비스 | **Java 21** | virtual thread와 pattern matching을 안정 기능으로 사용하면서도 생태계가 충분히 성숙했다. |
+| 제품 인증이나 핵심 의존성이 Java 21을 막는 서비스 | **Java 17** | Java 8을 벗어나는 현실적인 호환성 단계다. 제약이 없다면 신규 서비스의 최종 목표로 고르지는 않는다. |
+| 애플리케이션 또는 공급자 제약으로 이전할 수 없는 시스템 | **Java 8 임시 유지** | 보안 업데이트가 제공되는 배포판을 유지하되 업그레이드 일정과 예산을 별도로 확보해야 한다. |
+
+한 문장으로 줄이면 **전체 스택이 지원하면 25, 아직 지원하지 않으면 21,
+호환성 제약이 있으면 17, 8은 이전 계획이 있는 유지 대상**이다. 다만 실제
+지원 종료일은 버전 번호가 아니라 선택한 JDK 배포판과 계약에 따라 달라진다.
+
+## LTS를 해석할 때 주의할 점
+
+Java SE 사양과 OpenJDK 프로젝트가 모든 배포판에 하나의 LTS 기간을
+부여하는 것은 아니다. OpenJDK의 JDK 17, 21, 25 릴리스 페이지도
+“대부분의 공급자에게 LTS가 될 것”이라고 표현한다. 실제 binary, 보안
+업데이트 주기, 지원 운영체제와 종료일은 공급자가 정한다.
+
+예를 들어 [Microsoft Build of OpenJDK 지원
+로드맵](https://learn.microsoft.com/en-us/java/openjdk/support)은 Microsoft
+배포판의 17, 21, 25를 LTS로 분류한다. 같은 문서에서 Java 8은 Microsoft
+Build의 LTS 표에 넣지 않고 일부 Azure 시나리오에 Eclipse Temurin 기반
+binary와 container image를 사용한다고 설명한다. 이는 Microsoft 배포판과
+명시된 Azure 시나리오의 정책이며 다른 공급자의 Java 8 지원 기간을 뜻하지
+않는다.
+
+버전을 선택할 때는 다음을 함께 확인한다.
+
+1. JDK 배포판의 보안 업데이트 주기와 지원 종료일
+2. 프레임워크, build plugin, annotation processor와 native library의 지원
+3. APM, profiler, 보안 agent와 동적 agent loading 방식의 호환성
+4. base image, CPU architecture와 운영체제 지원
+5. 장애 시 JDK image와 JVM option을 함께 되돌릴 수 있는지
+
+## 버전별 누적 주요 변화
+
+아래 표의 Java 17, 21, 25 행은 해당 버전에만 추가된 기능이 아니라 **앞선
+비교 버전 이후 누적된 주요 변화**를 요약한다.
+
+| 버전 | 언어와 표준 API | 동시성·JVM·운영 변화 | 적용 관점 |
+|---|---|---|---|
+| **Java 8** | lambda와 method reference, interface default method, Stream, `Optional`, `java.time` | `CompletableFuture`, PermGen을 대체한 Metaspace | 함수형 collection 처리와 비동기 조합의 출발점이지만 이후 10년 이상의 런타임·진단 개선은 포함하지 않는다. |
+| **Java 17**
(9~17 누적) | module system, `var`, 표준 HTTP Client, switch expression, text block, record, `instanceof` pattern matching, sealed class | JDK 9부터 HotSpot server-class 구성은 G1이 기본 GC, JDK 15부터 ZGC가 production 기능, helpful `NullPointerException`, elastic Metaspace, JDK internal의 강한 캡슐화 | Java 8 코드와 라이브러리의 internal API·제거 모듈 의존성을 정리하면서 데이터 모델을 간결하게 만들 수 있다. |
+| **Java 21**
(18~21 누적) | record pattern, pattern matching for `switch`, sequenced collection, 기본 charset UTF-8 | virtual thread, generational ZGC 도입, 동적 agent loading 경고 | blocking I/O 서비스의 thread-per-request 모델을 유지하면서 동시성 규모를 키울 수 있다. |
+| **Java 25**
(22~25 누적) | unnamed variable/pattern, FFM API, Class-File API, Stream Gatherer, Scoped Value, module import, flexible constructor body | virtual thread pinning 완화, AOT cache와 profiling, compact object header, opt-in generational Shenandoah, JFR 개선, Security Manager 비활성화, 32-bit x86 port 제거 | 새 기능뿐 아니라 agent, JNI, 보안 설정, architecture와 관측 도구의 변경도 함께 검증해야 한다. |
+
+### Java 25에서도 안정 기능이 아닌 항목
+
+JDK 25의 **Structured Concurrency는 fifth preview**, Stable Value와 PEM
+encoding은 preview, primitive type pattern은 third preview, Vector API는
+tenth incubator다. JFR CPU-time profiling도 experimental 기능이다. 이런
+기능은 `--enable-preview` 또는 별도 option이 필요한 실험 경로에서 검토하고
+운영 기본 구현 패턴으로 삼지 않는다.
+
+Generational Shenandoah는 JDK 25에서 experimental 단계를 벗어난 product
+기능이지만 기본 mode가 아니다. 사용하려면
+`-XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational`을 지정해야 하며,
+Shenandoah의 기본은 single-generation mode다.
+
+또한 Java 21에 generational ZGC가 도입됐다고 해서 당시 기본 GC가 ZGC로
+바뀐 것은 아니다. JDK 21에서는 ZGC를 선택하고 generational mode를 별도로
+활성화해야 했다. 이후 JDK 23에서 generational mode가 ZGC의 기본이 됐고,
+JDK 24에서 non-generational mode가 제거됐다.
+
+## 어떤 버전을 선택할까
+
+### 신규 서비스
+
+Java 25를 우선 검토한다. 단, “최신이므로 선택”하는 것이 아니라 다음
+조건이 모두 충족되어야 한다.
+
+- 배포판의 Java 25 지원 기간이 서비스 수명주기와 맞는다.
+- framework와 모든 build·runtime dependency가 Java 25를 지원한다.
+- APM, profiler, 보안 agent와 container base image가 검증됐다.
+- 운영 환경과 같은 CPU architecture, memory limit, JVM option으로 부하
+ 테스트를 통과했다.
+
+하나라도 막히면 Java 21을 선택하고 막힌 구성요소와 재검토 날짜를
+기록한다. Java 17은 조직 표준 또는 제품 인증 때문에 21을 쓸 수 없을 때의
+전환점으로 보는 편이 좋다.
+
+### 기존 서비스
+
+목표 버전을 먼저 고정한 뒤 Java 8 → 17 → 21 → 25를 모두 운영에 순차
+배포할 필요는 없다. 중간 버전은 호환성 문제를 좁히는 진단 단계로 사용할
+수 있지만, 운영 전환은 테스트와 rollback이 준비된 목표 LTS로 직접
+진행할 수 있다.
+
+Java 8 서비스라면 JDK 9의 module system과 JDK 17의 강한 캡슐화, JDK 11의
+Java EE·CORBA module 제거가 주요 경계다. Java 17 서비스라면 agent,
+reflection, JNI, virtual thread 사용 library와 제거 예정 API를 중심으로
+21 또는 25 호환성을 확인한다.
+
+## 최신 JDK가 성능에 유리할 수 있는 이유
+
+최신 JDK는 누적된 JIT, GC, runtime, platform, 진단 개선을 포함하므로 더
+나은 출발점이 될 수 있다. 하지만 **같은 코드가 항상 더 빨라진다는 보장은
+없다**.
+
+| 개선 경로 | 기대할 수 있는 효과 | 보장하지 않는 것 |
+|---|---|---|
+| JIT와 runtime 최적화 | 장기 실행 처리량, code generation, platform별 실행 효율 개선 가능성 | framework와 workload가 달라도 같은 향상률 |
+| G1·ZGC·Shenandoah 개선 | pause, allocation stall, heap 회수와 memory overhead 개선 가능성 | 모든 heap 크기에서 ZGC가 G1보다 높은 처리량 |
+| virtual thread | blocking I/O가 많은 서비스에서 적은 platform thread로 더 많은 동시 작업 처리 | CPU-bound 계산 가속, 개별 요청 latency 감소, downstream 용량 증가 |
+| AOT class loading·linking과 method profiling | 반복되는 시작 경로의 startup과 warm-up 단축 가능성 | native image와 같은 실행 모델, training 없이 자동 적용 |
+| compact object header | 객체가 많은 workload에서 heap footprint와 cache locality 개선 가능성 | 기본 활성화 또는 모든 객체 구성에서 같은 효과 |
+| JFR와 진단 개선 | 병목과 회귀를 더 낮은 비용으로 관찰해 튜닝 시간을 줄일 가능성 | 관측 기능 자체에 의한 application 처리량 증가 |
+
+특히 JDK 25의 compact object header는 experimental 단계를 벗어난 product
+기능이지만 **기본 object-header layout은 아니다**. 명시적으로 활성화하고
+heap 크기, object 수, identity hash code 사용과 실제 latency를 비교해야
+한다.
+
+JDK 24부터는 `synchronized` 영역에서 blocking하는 virtual thread가
+platform thread에 pinning되는 경우가 대부분 제거됐다. 이는 Java 21보다
+기존 library와 함께 virtual thread를 적용하기 쉬워지는 변화지만, native
+code 호출 같은 남은 pinning 원인과 외부 시스템의 동시성 한도까지 없애지는
+않는다.
+
+JDK 24의 AOT class loading·linking은 training run에서 얻은 class 상태를
+cache에 저장해 startup을 줄이는 기능이다. JDK 25의 AOT method profiling은
+같은 cache에 실행 profile을 더해 JIT warm-up을 앞당긴다. 두 기능 모두
+production 경로를 대표하는 training이 필요하며 native-image compile로
+바뀌는 것은 아니다.
+
+## 업그레이드는 호환성과 현대화를 분리한다
+
+한 변경에서 JDK, framework major version, application 구조와 동시성 모델을
+모두 바꾸면 회귀 원인을 구분하기 어렵다. 다음 두 단계를 별도 배포 가능한
+변경으로 관리한다.
+
+### 1단계: 기존 동작을 목표 JDK에서 재현
+
+1. JDK 배포판과 지원 기간, OS·architecture, container image를 고정한다.
+2. Maven·Gradle과 compiler plugin, framework, library, annotation
+ processor, test tool을 목표 JDK 지원 버전으로 올린다.
+3. APM·profiler·보안 agent, JNI/JNA와 reflection 기반 library를 확인한다.
+4. 제거된 module·API, JDK internal 접근과 더 이상 유효하지 않은 JVM
+ option을 찾는다.
+5. 기능 회귀, startup, 부하, 장시간 안정성 테스트를 기존 구현 그대로
+ 통과시킨다.
+
+JDK에 포함된 도구로 정적 의존성을 먼저 찾을 수 있다.
+
+```bash
+jdeps --jdk-internals build/app.jar
+jdeprscan --release 25 build/app.jar
+```
+
+`jdeps`에 나오지 않는 reflection 접근과 runtime agent 동작은 실제 통합
+테스트와 startup log로 보완한다. 경고를 무시하는 `--add-opens`를 영구
+해결책으로 늘리기보다 library upgrade 또는 public API 전환을 우선한다.
+
+### 2단계: 안정 기능을 작은 단위로 도입
+
+runtime 전환이 안정된 뒤 record, sealed type, pattern matching, virtual
+thread와 Scoped Value를 독립 변경으로 도입한다. 각 변경은 동작 테스트와
+성능 지표를 다시 비교할 수 있어야 한다. preview 기능은 production
+baseline과 분리한다.
+
+## 버전 업그레이드 후 권장 구현 패턴
+
+### 불변 값은 record로 의도를 드러낸다
+
+보일러플레이트를 줄이기 위해 모든 class를 record로 바꾸는 것이 아니라,
+값 자체가 정체성이고 불변 data carrier인 타입에 사용한다.
+
+```java
+public record CustomerId(String value) {
+ public CustomerId {
+ if (value == null || value.isBlank()) {
+ throw new IllegalArgumentException("value must not be blank");
+ }
+ }
+}
+```
+
+가변 lifecycle, lazy loading, 상속 기반 proxy가 필요한 entity에는 기존
+class가 더 적합할 수 있다.
+
+### 닫힌 도메인은 sealed type과 exhaustive switch로 모델링한다
+
+허용되는 결과가 정해진 도메인은 sealed hierarchy로 표현하고 Java 21의
+record pattern과 pattern matching for `switch`를 함께 사용한다.
+
+```java
+sealed interface PaymentResult permits Approved, Declined {}
+
+record Approved(String approvalId) implements PaymentResult {}
+
+record Declined(String reason) implements PaymentResult {}
+
+final class PaymentMessages {
+ private PaymentMessages() {}
+
+ static String message(PaymentResult result) {
+ return switch (result) {
+ case Approved(var approvalId) -> "approved: " + approvalId;
+ case Declined(var reason) -> "declined: " + reason;
+ };
+ }
+}
+```
+
+새 subtype이 추가되면 compiler가 누락된 분기를 찾을 수 있다. 외부 plugin이
+임의 subtype을 추가해야 하는 확장 지점에는 sealed type을 사용하지 않는다.
+
+### blocking I/O에는 virtual-thread-per-task를 검토한다
+
+virtual thread 자체를 pool로 재사용하지 않는다. 작업마다 virtual thread를
+만들되 connection pool, rate limiter, semaphore 또는 downstream quota로
+실제 외부 호출 수를 제한한다.
+
+```java
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.Semaphore;
+
+final class DownstreamCalls {
+ private static final Semaphore DOWNSTREAM_LIMIT = new Semaphore(100);
+
+ private DownstreamCalls() {}
+
+ static String fetch(Callable request) throws Exception {
+ DOWNSTREAM_LIMIT.acquire();
+ try {
+ return request.call();
+ } finally {
+ DOWNSTREAM_LIMIT.release();
+ }
+ }
+
+ static List fetchAll(List> requests)
+ throws InterruptedException, ExecutionException {
+ try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
+ var futures = requests.stream()
+ .map(request -> executor.submit(() -> fetch(request)))
+ .toList();
+
+ try {
+ var results = new ArrayList(futures.size());
+ for (var future : futures) {
+ results.add(future.get());
+ }
+ return List.copyOf(results);
+ } catch (InterruptedException error) {
+ cancelAll(futures);
+ Thread.currentThread().interrupt();
+ throw error;
+ } catch (ExecutionException error) {
+ cancelAll(futures);
+ throw error;
+ }
+ }
+ }
+
+ private static void cancelAll(List extends Future>> futures) {
+ futures.forEach(future -> future.cancel(true));
+ }
+}
+```
+
+각 `Callable`이 사용하는 HTTP·database client에도 connect·request·read
+deadline을 설정해야 한다. `cancel(true)`는 interrupt에 협조하는 작업만
+중단할 수 있으므로 client timeout을 대신하지 않는다. CPU-bound 작업의
+병렬도는 CPU core 수를 기준으로 제한한다. virtual thread 수를 늘려도 CPU
+core와 downstream connection 수는 늘어나지 않는다.
+
+### Java 25에서는 같은 thread의 불변 context에 Scoped Value를 검토한다
+
+Scoped Value는 Java 25에서 final이 됐다. 한 thread의 호출 경로에서 읽기
+전용 context를 전달할 때 mutable `ThreadLocal`보다 명확한 lifetime을
+제공한다.
+
+```java
+final class RequestContext {
+ private static final ScopedValue TRACE_ID =
+ ScopedValue.newInstance();
+
+ private RequestContext() {}
+
+ static void handle(String traceId, Runnable action) {
+ ScopedValue.where(TRACE_ID, traceId).run(action);
+ }
+
+ static String traceId() {
+ return TRACE_ID.get();
+ }
+}
+```
+
+framework가 tracing·security context 전파 방식을 이미 제공한다면 그
+lifecycle을 우선한다. 같은 context를 framework API와 Scoped Value에
+중복 저장하지 않는다. 일반 `Thread`나 executor에 제출한 작업은 binding을
+자동 상속하지 않는다. Child task 상속은 JDK 25에서도 preview인 Structured
+Concurrency의 `StructuredTaskScope`로 생성한 task에 한정되므로, 이 문서의
+운영 기본 패턴에는 포함하지 않는다.
+
+## 성능 검증 체크리스트
+
+기준 JDK와 목표 JDK에서 다음 조건을 같게 유지한다.
+
+- application artifact, framework와 dependency version
+- traffic model, test data, test duration과 concurrency
+- CPU architecture, container CPU·memory limit
+- JVM option, heap 크기, GC 종류와 warm-up 정책
+- 외부 API·database의 용량과 connection pool
+
+다음 지표를 함께 비교한다.
+
+- throughput과 error rate
+- p50, p95, p99 latency
+- startup time과 steady state까지의 warm-up 시간
+- CPU 사용량, RSS와 heap 사용량
+- allocation rate, GC 횟수와 pause 분포
+- thread·connection 수와 downstream saturation
+
+평균값만 비교하지 말고 여러 회차의 분포와 confidence interval을 확인한다.
+운영 반영은 canary 또는 점진적 배포로 진행하고, 기준을 벗어나면 JDK image와
+JVM option을 함께 되돌린다.
+
+## 제약 사항
+
+- 이 문서는 framework별 지원 matrix나 JDK 공급자별 가격·전체 지원 기간을
+ 비교하지 않는다.
+- Java 11은 요청된 비교 범위에서 제외했다. Java 8에서 17로 이동할 때
+ 9~11의 module·API 변화는 포함했다.
+- 성능 수치는 특정 application을 측정한 결과가 아니므로 보편적인 향상률을
+ 제시하지 않는다.
+- feature 상태는 2026-09-16에 확인했다. preview·incubator 기능은 이후
+ 릴리스에서 변경되거나 제거될 수 있다.
+
+## 공식 참고 자료
+
+- [Microsoft Build of OpenJDK 지원
+ 로드맵](https://learn.microsoft.com/en-us/java/openjdk/support)
+- [JDK 8 프로젝트](https://openjdk.org/projects/jdk8/)와 [JDK 8 주요
+ 기능](https://openjdk.org/projects/jdk8/features)
+- [JDK 17 프로젝트](https://openjdk.org/projects/jdk/17/)와 [JDK 11 이후
+ JEP](https://openjdk.org/projects/jdk/17/jeps-since-jdk-11)
+- [JDK 21 프로젝트](https://openjdk.org/projects/jdk/21/)와 [JDK 17 이후
+ JEP](https://openjdk.org/projects/jdk/21/jeps-since-jdk-17)
+- [JDK 25 프로젝트](https://openjdk.org/projects/jdk/25/)와 [JDK 21 이후
+ JEP](https://openjdk.org/projects/jdk/25/jeps-since-jdk-21)
+- [Virtual Threads](https://openjdk.org/jeps/444), [Generational
+ ZGC](https://openjdk.org/jeps/439), [virtual thread pinning
+ 개선](https://openjdk.org/jeps/491)
+- [Ahead-of-Time Class Loading &
+ Linking](https://openjdk.org/jeps/483), [Ahead-of-Time Method
+ Profiling](https://openjdk.org/jeps/515), [Scoped
+ Values](https://openjdk.org/jeps/506), [Compact Object
+ Headers](https://openjdk.org/jeps/519), [Generational
+ Shenandoah](https://openjdk.org/jeps/521)