[TIPS] Java 8·11 ➔ Java 17·21 LTS 업그레이드 완벽 가이드: 모듈 시스템(Jigsaw)·InaccessibleObjectException 리플렉션 오류·제거된 JVM 옵션 및 가상 스레드 트러블슈팅
💡 핵심 요약: 엔터프라이즈 Java 생태계가 Java 8과 Java 11의 장기 지원 종료 및 Spring Boot 3.x, Quarkus 3.x의 Java 17+ 의무화 정책에 따라 Java 17 및 Java 21 LTS로 급격히 전환되고 있습니다. 그러나 수년 동안 안정적으로 운영되던 레거시 시스템을 최신 LTS로 무턱대고 판올림하면, JDK 9에서 도입된 모듈 시스템(Project Jigsaw)의 강력한 내부 캡슐화 강제(JDK 16/17), --illegal-access=permit 플래그 완전 삭제, CMS GC 등 레거시 가비지 컬렉터와 JVM 옵션 폐지(Unrecognized VM option), 그리고 Java 21 가상 스레드(Virtual Threads) 도입에 따른 서드파티 라이브러리의 캐리어 스레드 핀닝(Pinning) 문제로 인해 애플리케이션 시작조차 못 하고 즉각 크래시가 발생합니다. 본 가이드에서는 Java 8/11 코드가 17/21에서 깨지는 기술적 근본 원인부터 최신 LTS 공식 표준, 4단계 빌드 및 컨테이너 환경 설정, 그리고 실무에서 반드시 맞닥뜨리는 4대 치명적 장애 트러블슈팅과 5단계 마이그레이션 체크리스트를 총정리합니다.
1. 왜 과거 Java 8/11 코드는 Java 17/21에서 깨지는가?
Java 8에서 11로 넘어갈 때도 일부 변경점이 있었지만, Java 17 및 Java 21로의 도약은 단순한 라이브러리 추가가 아닌 JVM 런타임의 근본적 보안 격리와 아키텍처 개편을 동반합니다. 과거에 허용되던 편법과 레거시 튜닝 옵션들이 최신 JDK에서 철저하게 차단된 기술적 원인은 다음과 같습니다.
1) 모듈 시스템(Project Jigsaw)의 강력한 캡슐화(Strong Encapsulation) 강제
JDK 9에서 도입된 JPMS(Java Platform Module System, Project Jigsaw)는 JDK 내부의 비공개 API(예: sun.misc.*, java.base/java.lang.* 내부 필드)에 대한 무분별한 리플렉션 접근을 제한하도록 설계되었습니다.
- Java 9 ~ 15: 과도기적 하위 호환성을 위해 경고(Warning)만 출력하고 불법적인 리플렉션 접근을 허용하는
--illegal-access=permit이 기본 활성화되어 있었습니다. - Java 16 (JEP 396): 기본값이
deny로 전환되어 명시적 허용 없이는 예외를 발생시키기 시작했습니다. - Java 17 (JEP 403): 강력한 캡슐화(Strongly Encapsulate JDK Internals by Default)가 전면 적용되었으며, 하위 호환성을 지탱하던
--illegal-access옵션 자체가 완전히 폐지되었습니다.
이로 인해 과거 CGLIB, cglib-nodep, 레거시 Gson, MyBatis, Kryo, 하이버네이트 구버전 등이 Field.setAccessible(true)를 통해 java.base 내부를 조작하던 모든 코드가 런타임에 즉시 InaccessibleObjectException을 뱉으며 중단됩니다.
2) CMS(Concurrent Mark Sweep) GC 및 레거시 JVM 플래그 완전 삭제
과거 Java 8 시절 대규모 웹 서비스의 지연 시간 단축을 위해 필수적으로 사용되던 CMS GC(-XX:+UseConcMarkSweepGC)는 Java 9에서 Deprecated 되었고, Java 14(JEP 363)에서 JVM 코드베이스에서 완전히 삭제되었습니다. 최신 HotSpot JVM은 인식할 수 없는 JVM 플래그를 발견하면 경고로 넘어가지 않고 즉시 프로세스 부팅을 중단(Unrecognized VM option)하므로, 기존 쉘 스크립트나 Kubernetes 배포 매니페스트에 남아 있던 과거 튜닝 플래그들이 컨테이너 무한 재시작(CrashLoopBackOff)을 유발합니다.
3) Security Manager, Nashorn JS 엔진, RMI 컴파일러 제거
엔터프라이즈 환경에서 레거시 연동용으로 잔존하던 컴포넌트들이 정리되었습니다:
- Nashorn 자바스크립트 엔진: Java 15(JEP 372)에서 완전 제거 (필요 시 GraalVM JS로 대체).
- Security Manager: Java 17(JEP 411)에서 비권장(Deprecated for Removal) 처리되었으며, Java 21+ 환경에서는 활성화 시 경고 및 일부 API 무력화 진행.
- Applet 및 RMI Activation: 완전히 제거되어 레거시 EJB/RMI 클라이언트 패키지 재검토 필수.
2. 최신 공식 표준 해법: Java 17 & 21 LTS 런타임 아키텍처
현대 엔터프라이즈 백엔드 아키텍처에서 Java 17과 Java 21 LTS를 도입할 때 준수해야 하는 공식 표준 기준은 다음과 같습니다.
1) Java 17 LTS: 엔터프라이즈 표준 베이스라인
- Record 클래스 (JEP 395): DTO와 값 객체(VO)의 보일러플레이트 코드를 제거하고 불변성(Immutability)을 언어 차원에서 보장합니다.
- 봉인된 클래스 (Sealed Classes, JEP 409): 상속 계층 구조를 제한하여 도메인 모델의 안전성과 가독성을 높입니다.
- 텍스트 블록 (Text Blocks, JEP 378): 복잡한 SQL, JSON, YAML 문자열을 이스케이프 없이 직관적으로 작성합니다.
- 기본 가비지 컬렉터 G1 GC 최적화: Java 8 대비 향상된 동적 힙 튜닝, 병렬 풀 GC 지원으로 별도 튜닝 없이도 대부분의 워크로드에서 안정적인 지연 시간을 보장합니다.
2) Java 21 LTS: 고성능 동시성 및 차세대 런타임
- 가상 스레드 (Virtual Threads / Project Loom, JEP 444): OS 스레드와 1:1로 매핑되던 전통적인 플랫폼 스레드(Platform Thread)의 한계를 극복하고, JVM 내부 스케줄러가 수백만 개의 가상 스레드를 경량 관리합니다. I/O 블로킹 작업에서 스레드 풀 고갈을 방지하고 전통적인 동기식 프로그래밍 모델로 비동기 논블로킹급 처리량을 달성합니다.
- 세대별 ZGC (Generational ZGC, JEP 439): 테라바이트급 대용량 힙 메모리에서도 GC 일시 정지(STW, Stop-The-World) 시간을 1밀리초(ms) 미만으로 억제하며, 영(Young)과 올드(Old) 세대를 분리 수집하여 처리량과 응답성을 동시에 확보합니다.
- 시퀀스드 컬렉션 (Sequenced Collections, JEP 431): 정렬 순서가 보장되는 컬렉션 인터페이스 표준화로 일관된 첫/마지막 요소 접근 API 제공.
3. 버전별 JVM 호환성 매트릭스
Java 8부터 최신 21 LTS까지의 핵심 플랫폼 변경 사항과 권장 빌드 도구 매트릭스입니다.
| 항목 (Feature) | Java 8 (레거시) | Java 11 LTS (과도기) | Java 17 LTS (표준) | Java 21 LTS (최신 권장) |
|---|---|---|---|---|
| 바이트코드 버전 (Class Version) | 52.0 | 55.0 | 61.0 | 65.0 |
| JPMS 모듈 캡슐화 | 미도입 (Classpath) | 도입 (--illegal-access=permit) |
완전 강제 (deny, 플래그 제거) | 완전 강제 (deny) |
| 기본 가비지 컬렉터 | Parallel GC | G1 GC (초기 형태) | G1 GC (성능 성숙) | G1 GC / Generational ZGC |
| CMS GC 지원 여부 | 기본/선택 지원 | Deprecated | 완전 제거 (옵션 사용 시 크래시) | 완전 제거 |
| 동시성 모델 (Concurrency) | 플랫폼 스레드 (1:1 OS) | 플랫폼 스레드 (1:1 OS) | 플랫폼 스레드 (1:1 OS) | Virtual Threads 공식 탑재 (Loom) |
| 최소 권장 Gradle 버전 | Gradle 4.x ~ 6.x | Gradle 5.0+ | Gradle 7.3+ (권장 8.x) | Gradle 8.5+ (최신 8.10+) |
| 최소 권장 Maven 버전 | Maven 3.2.x | Maven 3.5.x | Maven 3.8.x+ | Maven 3.9.x+ |
| 최소 Spring Framework | Spring 4.x / 5.x | Spring 5.1+ | Spring 6.0+ (Spring Boot 3.0+) | Spring 6.1+ (Spring Boot 3.2+) |
4. 실무 프로덕션 설정 코드
Java 17/21로의 안전한 전환을 위해 빌드 스크립트(build.gradle, pom.xml), 프로덕션 Dockerfile 멀티스테이지 빌드 및 JVM 옵션 파일을 실무 표준으로 재구성합니다.
1) build.gradle (Gradle 8.x 현대적 도구 체인 설정)
Java 8/11에서 컴파일되던 구형 Gradle 스크립트를 Java 17/21 Java Toolchain 표준 방식으로 전환합니다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.4'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'com.example'
version = '1.0.0-SNAPSHOT'
// Java Toolchain을 통한 빌드 환경 표준화 (로컬 JDK 설치 버전과 무관하게 자동 프로비저닝)
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21) // 또는 Java 17
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// Lombok 최신 버전 명시 (Java 21 javac 컴파일러 호환 필수)
compileOnly 'org.projectlombok:lombok:1.18.34'
annotationProcessor 'org.projectlombok:lombok:1.18.34'
testCompileOnly 'org.projectlombok:lombok:1.18.34'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.34'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('compileJava') {
options.encoding = 'UTF-8'
options.compilerArgs << '-parameters' // 리플렉션 파라미터 이름 유지
}
tasks.named('test') {
useJUnitPlatform()
// 단위 테스트 실행 시 서드파티 모의 객체(Mockito/ByteBuddy)용 리플렉션 허용 플래그
jvmArgs([
'-XX:+EnableDynamicAgentLoading', // Java 21 동적 에이전트 로딩 경고 억제
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
'--add-opens', 'java.base/java.util=ALL-UNNAMED'
])
}
2) pom.xml (Maven Compiler 플래그 설정)
Maven 빌드 환경에서는 maven-compiler-plugin 3.11+ 버전을 사용하여 릴리스 버전을 명시합니다.
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>modern-java-service</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<java.version>21</java.version>
<maven.compiler.release>21</maven.compiler.release>
<lombok.version>1.18.34</lombok.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>${java.version}</release>
<parameters>true</parameters>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
3) 프로덕션 Dockerfile 멀티스테이지 빌드 및 최신 JVM 플래그
경량화된 Eclipse Temurin 컨테이너 이미지를 기반으로, 컨테이너 인식(Container-Aware) 최신 플래그를 적용합니다.
# 1단계: 빌드 환경 (Eclipse Temurin JDK 21 Alpine) FROM eclipse-temurin:21-jdk-alpine AS builder WORKDIR /workspace COPY gradlew . COPY gradle gradle COPY build.gradle settings.gradle ./ RUN ./gradlew dependencies --no-daemon COPY src src RUN ./gradlew bootJar --no-daemon -x test # 2단계: 런타임 환경 (Eclipse Temurin JRE 21 Alpine) FROM eclipse-temurin:21-jre-alpine WORKDIR /app # 비루트(Non-root) 전용 시스템 계정 생성 및 보안 권한 부여 RUN addgroup -S appgroup && adduser -S appuser -G appgroup USER appuser:appgroup COPY --from=builder /workspace/build/libs/*.jar app.jar COPY jvm.options /app/jvm.options # 컨테이너 메모리 비율(75%) 기반 튜닝 및 안전한 GC 가동 ENV JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75.0 -XX:InitialRAMPercentage=50.0 -XX:+UseG1GC -XX:+ExitOnOutOfMemoryError" ENTRYPOINT ["sh", "-c", "java @/app/jvm.options -jar app.jar"]
4) 실무 jvm.options (Java 17/21 최적화 및 리플렉션 안전 개방)
외부 서드파티 라이브러리(레거시 매퍼, 캐시 엔진)의 JPMS 리플렉션 에러를 방지하고 저지연 운영을 보장하는 JVM 옵션 파일입니다.
# --- Garbage Collection & Memory --- -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+ParallelRefProcEnabled # Java 21에서 초저지연(Ultra-low latency)이 필요한 경우 G1GC 대신 Generational ZGC 사용 가능: # -XX:+UseZGC # -XX:+ZGenerational # --- Out Of Memory & Crash Diagnostics --- -XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/dumps/heap_dump.hprof # --- JPMS Strong Encapsulation Reflection Opens (서드파티 호환성) --- --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.base/sun.net.util=ALL-UNNAMED
5. 자주 겪는 4대 치명적 에러 트러블슈팅
Java 8/11에서 17/21로 마이그레이션할 때 가장 빈번하게 발생하는 4대 치명적 장애와 해결책입니다.
1) InaccessibleObjectException: module java.base does not "opens java.lang"
- 발생 증상: Spring 컨텍스트 초기화 또는 JSON 직렬화/역직렬화(Jackson, Gson) 시점에 다음 예외가 발생하며 애플리케이션 시작 실패.
java.lang.reflect.InaccessibleObjectException: Unable to make field private final byte[] java.lang.String.value accessible:
module java.base does not "opens java.lang" to unnamed module @4f023edb
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
at java.base/java.lang.reflect.Field.setAccessible(Field.java:178)
at com.esotericsoftware.kryo.util.UnsafeUtil.<clinit>(UnsafeUtil.java:42)
- 근본 원인: Java 16/17부터 JDK 내부 모듈(
java.base)이 명시되지 않은 모듈(애플리케이션 클래스패스)에 대해 private 필드 접근을 철저히 차단합니다. - 해결책 1 (정석): 서드파티 라이브러리 최신화
내부 리플렉션 대신 공식 MethodHandles나 표준 Java Bean 접근자를 사용하도록 라이브러리를 최신화합니다:
- Jackson:
2.15.0+이상으로 업그레이드.- Kryo:
5.5.0+이상으로 업그레이드. - ByteBuddy:
1.14.0+이상으로 업그레이드.
- Kryo:
- 해결책 2 (단기 운영 조치): JVM
--add-opens플래그 명시
서드파티 라이브러리를 즉시 교체하기 어려운 경우, JVM 실행 인자에 해당 모듈의 접근을 개방합니다:
java --add-opens java.base/java.lang=ALL-UNNAMED \
--add-opens java.base/java.util=ALL-UNNAMED \
-jar your-app.jar
2) Unrecognized VM option 'UseConcMarkSweepGC' 및 폐지된 JVM 옵션
- 발생 증상: 컨테이너 또는 프로세스 기동 직후 로그 한 줄을 남기고 프로세스 즉각 종료.
Unrecognized VM option 'UseConcMarkSweepGC' Error: Could not create the Java Virtual Machine. Error: A fatal exception has occurred. Program will exit.
- 근본 원인: CMS GC 관련 옵션 및 Java 8 시절의 PermGen 옵션이 Java 14 및 17에서 물리적으로 완전히 삭제되었습니다.
- 해결책: 기존 쉘 스크립트 및 Docker 시작 스크립트에서 폐지된 옵션을 삭제하고 최신 표준으로 교체합니다.
| 삭제 대상 폐지 옵션 (Java 8/11) | 원인 및 대체 최신 옵션 (Java 17/21) |
|---|---|
-XX:+UseConcMarkSweepGC |
Java 14 완전 삭제 ➔ -XX:+UseG1GC 또는 -XX:+UseZGC -XX:+ZGenerational |
-XX:CMSInitiatingOccupancyFraction=70 |
CMS 전용 옵션 ➔ G1GC 사용 시 -XX:InitiatingHeapOccupancyPercent=45 |
-XX:+CMSParallelRemarkEnabled |
CMS 전용 옵션 ➔ 삭제 (G1GC 기본 내장) |
-XX:PermSize=..., -XX:MaxPermSize=... |
Java 8부터 삭제된 PermGen 옵션 ➔ -XX:MaxMetaspaceSize=... |
-XX:+PrintGCDetails, -XX:+PrintGCTimeStamps |
구형 GC 로깅 ➔ 통합 로깅 프레임워크 -Xlog:gc*:file=/var/log/gc.log:time,tags:filecount=5,filesize=50m |
3) Lombok 컴파일 에러: NoSuchFieldError: JCImport
- 발생 증상: Gradle 또는 Maven 빌드 중 어노테이션 프로세서 단계에서 컴파일 실패.
java.lang.NoSuchFieldError: JCImport
at lombok.javac.JavacAST.importMatches(JavacAST.java:312)
at lombok.javac.HandlerLibrary.handleAnnotation(HandlerLibrary.java:189)
Caused by: java.lang.ClassNotFoundException: com.sun.tools.javac.tree.JCTree$JCImport
- 근본 원인: Lombok은
com.sun.tools.javac내부 트리를 직접 조작합니다. Java 17 및 Java 21에서 JDK 내부 컴파일러 AST 구조가 변경되었으며 모듈 캡슐화가 적용되었습니다. - 해결책: Lombok 버전을 Java 17/21을 완벽히 지원하는
1.18.30이상 (최신1.18.34권장)으로 판올림합니다.
// build.gradle compileOnly 'org.projectlombok:lombok:1.18.34' annotationProcessor 'org.projectlombok:lombok:1.18.34'
만약 구형 빌드 툴체인에서 jdk.compiler 접근 오류가 지속된다면 컴파일러 인자에 모듈 개방을 추가합니다:
tasks.withType(JavaCompile).configureEach {
options.fork = true
options.forkOptions.jvmArgs += [
'--add-opens', 'jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED',
'--add-opens', 'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED',
'--add-opens', 'jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED',
'--add-opens', 'jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED',
'--add-opens', 'jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED',
'--add-opens', 'jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED',
'--add-opens', 'jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED',
'--add-opens', 'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED',
'--add-opens', 'jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED',
'--add-opens', 'jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED'
]
}
4) Virtual Thread 환경의 캐리어 스레드 핀닝(Carrier Thread Pinning)
- 발생 증상: Java 21 가상 스레드(
spring.threads.virtual.enabled=true) 활성화 후 대규모 I/O 부하 발생 시 응답 지연(Latency Spike)이 치솟고 처리량이 급락. - 근본 원인: 가상 스레드가
synchronized블록 또는 메서드 내부에서 블로킹 I/O(네트워크 소켓 대기, 디스크 쓰기 등)를 수행하거나 JNI 네이티브 메서드를 호출하면, 가상 스레드가 기반 OS 스레드인 캐리어 스레드(Carrier Thread)에 고정(Pinning)되어 언마운트(Unmount)되지 못합니다. 이로 인해 워커 스레드 풀 전체가 고갈됩니다. - 핀닝 감지 JVM 옵션 활성화:
-Djdk.tracePinnedThreads=full # 또는 짧은 스택 출력: -Djdk.tracePinnedThreads=short
- 해결책:
synchronized를 Java 5부터 제공되는 모던 락인ReentrantLock으로 전면 교체합니다.
// ❌ [레거시 위험 코드] synchronized 내부에서 블로킹 I/O 호출 -> 캐리어 스레드 핀닝 발생
public class LegacyPaymentClient {
private final Object lock = new Object();
public String sendPaymentRequest(PaymentDto request) {
synchronized (lock) { // ❌ Virtual Thread 핀닝 유발 지점!
return restTemplate.postForObject("/pay", request, String.class); // I/O 블로킹
}
}
}
// [현대 표준 해결책] ReentrantLock 전환 -> 가상 스레드가 안전하게 언마운트됨
import java.util.concurrent.locks.ReentrantLock;
public class ModernPaymentClient {
private final ReentrantLock lock = new ReentrantLock();
public String sendPaymentRequest(PaymentDto request) {
lock.lock();
try {
return restTemplate.postForObject("/pay", request, String.class); // 논블로킹 스위칭 정상 작동
} finally {
lock.unlock();
}
}
}
실무 팁: Virtual Thread 도입 시 필수 라이브러리 버전 가이드
Virtual Thread를 운영 환경에 도입할 때는 서드파티 라이브러리가 내부적으로 synchronized 기반 블로킹 I/O를 사용하는지 반드시 확인해야 합니다:
- 데이터베이스 드라이버: MySQL Connector/J
8.2.0+이상 또는 PostgreSQL JDBC42.6.0+이상 필수 (구버전은 소켓 I/O 시synchronized사용으로 핀닝 유발). - HTTP 클라이언트: Apache HttpClient 4.x(동기식) 대신 Apache HttpClient 5.x 또는 JDK 내장
java.net.http.HttpClient권장. - 커넥션 풀링: HikariCP
5.1.0+이상 권장 (가상 스레드 스케줄러와의 락 경합 최적화 반영). - 스레드 로컬(ThreadLocal) 남용 주의: 수십만 개의 가상 스레드마다 무거운
ThreadLocal객체가 할당되면 심각한 힙 메모리 낭비가 발생하므로, Java 21의 Scoped Values(JEP 446) 프리뷰 기능 또는 경량 불변 컨텍스트 전달 패턴을 권장합니다.
6. 빌드 검증 및 5단계 마이그레이션 체크리스트
운영 무중단과 안전한 배포를 위해 단계별 전환 절차를 표준화하여 실행합니다.
[Java 17 / 21 LTS 5단계 실무 마이그레이션 파이프라인] [1단계: 종속성 진단] ──► [2단계: 빌드 툴체인] ──► [3단계: JVM 옵션 정제] ──► [4단계: 테스트 검증] ──► [5단계: 카나리 배포] - jdeps 내부 의존 검사 - Gradle 8.5+ 업그레이드 - CMS/구형 옵션 완전 제거 - @VisibleForTesting 점검 - JVM 메트릭 모니터링 - 서드파티 라이브러리 점검 - JDK Toolchain 설정 - G1GC/ZGC 최적화 플래그 - Mockito 최신 에이전트 확인 - 가상 스레드 핀닝 추적
마이그레이션 전 필수 체크리스트
- 1단계: JDK 내부 API 의존성 정적 분석 (
jdeps)- JDK 내장 도구인
jdeps를 실행하여 프로젝트 jar 내부에 폐지되거나 캡슐화된 내부 API(sun.misc.*등) 호출이 존재하는지 사전 점검합니다: - 경고 출력된 서드파티 라이브러리 목록을 기반으로 최신 표준 대체재를 확인합니다.
jdeps --jdk-internals -cp "lib/*" build/libs/your-app.jar
- JDK 내장 도구인
- 2단계: 빌드 도구 및 어노테이션 프로세서 버전 확인
- Gradle은 최소
7.3+(Java 21의 경우8.5+), Maven은3.8+이상으로 업그레이드되었는가? - Lombok은
1.18.34, MapStruct는1.5.5.Final+이상으로 최신화되었는가? - 빌드 스크립트에 Java Toolchain을 적용하여 로컬 및 CI/CD 서버 환경과의 일관성을 확보했는가?
- Gradle은 최소
- 3단계: JVM 실행 옵션 파일 전수 정제
- 배포 스크립트, Helm Chart, Dockerfile에서
-XX:+UseConcMarkSweepGC,-XX:CMSInitiatingOccupancyFraction,PermSize플래그가 전면 삭제되었는가? - 구형 GC 로깅 플래그(
-XX:+PrintGCDetails)가-Xlog:gc*형식으로 교체되었는가? - 필수 불가결한 서드파티 레거시 라이브러리를 위해 최소한의
--add-opens가 지정되었는가? - 컨테이너 메모리 제한 인식 플래그(
-XX:MaxRAMPercentage=75.0)가 올바르게 적용되었는가?
- 배포 스크립트, Helm Chart, Dockerfile에서
- 4단계: 단위 및 통합 테스트 환경 검증
- JUnit 5 환경에서 Mockito 및 ByteBuddy 동적 에이전트 경고가 처리되었는가? (
-XX:+EnableDynamicAgentLoading) - 모든 단위 테스트와 스프링 컨텍스트 로딩(
@SpringBootTest)이 Java 17/21 바이트코드 환경에서 성공하는가? - H2 Database 등 인메모리 테스트 DB가 최신 Java 17/21 릴리스 버전으로 갱신되었는가?
- JUnit 5 환경에서 Mockito 및 ByteBuddy 동적 에이전트 경고가 처리되었는가? (
- 5단계: 프로덕션 카나리(Canary) 배포 및 가상 스레드 모니터링
- 운영 클러스터에 단일 인스턴스 카나리 배포를 진행하여 GC 일시 정지 시간(STW) 및 힙 사용 패턴을 프로메테우스/그라파나로 대조 관측하였는가?
- Java 21 가상 스레드 적용 시
-Djdk.tracePinnedThreads=full설정을 통해 JDBC 드라이버나 HTTP 클라이언트 라이브러리의 핀닝 발생 여부를 점검하였는가? (최신 MySQL Connector/J 8.2+, PostgreSQL JDBC 42.6+ 권장) - 프로덕션 APM(Pinpoint, Scouter, Datadog 등) 에이전트가 Java 17/21 바이트코드 계측을 지원하는 최신 버전인지 확인하였는가?