“Java 25? 우린 8 잘 쓰고 있는데요”
운영 서비스 한 곳을 들춰보면 의외로 자주 보는 풍경이다. Java 8로 시작했고, 11로 한 번 올렸다가 그대로 멈춰 있다. 17로 갈까 하다가 ‘Spring Boot 2 → 3’ 마이그레이션이 무서워서 미뤘다. 그러는 사이 Java는 이미 25 LTS, 그리고 26 GA까지 와 있다.
“Java 8/11에서 더 안 올린 지 오래됐어요. 근데 25는 너무 많이 점프 같아서…”
“record? sealed? virtual threads? 들어는 봤는데 실무에서 뭐가 좋아지는지 잘 모르겠어요.”
“Spring Boot 4 글 보니 Java 25 권장이라던데, 하나 올리면 다 올려야 하는 건가요?”
이 글은 그 사이에 자리 잡은 모던 Java의 변화를 ‘실무 영향 위주’로 정리한다. 시리즈 ‘Legacy Escape’의 2편이고, 1편(Spring Boot 4 업그레이드)에서 미뤄둔 ‘Java 25를 같이 올릴지’ 결정에 직접 도움이 될 글이다. 새 기능 카탈로그가 아니라 ‘내 코드에 무엇이 달라지는가’, 그리고 ‘왜 우리는 그동안 이걸 못 올렸는가’에 초점을 둔다.

왜 아직도 Java 8인가
‘한 줄 요약: 한국 SI만의 문제는 아니다. 전 세계적으로 Java 8은 빠르게 줄고 있고, 17·21이 빠르게 올라오는 중이다.’
“우리는 왜 이걸 미뤘을까”를 짚지 않으면 다음 결정도 같은 자리에서 멈춘다. Java 8 고착은 한국 SI에서 더 심하게 느껴지지만, 한국만의 문제는 아니다.
- JetBrains ‘State of Developer Ecosystem 2025’는 Java 8을 정기적으로 사용한다고 답한 비율을 31%로 집계했다. 2019년 83%, 2024년 44%에서 내려온 흐름이다. 같은 보고서에서 Java 17은 39%, Java 21은 40%다.
- Azul의 2025 State of Java 관련 보도도 Java 8 사용 비율이 2023년 40%에서 2025년 23%로 내려갔다고 정리한다.
- New Relic의 2024 프로덕션 데이터에서는 Java 17 사용 애플리케이션 비율이 35%까지 올라왔다.
방향은 분명하다. ‘해외도 레거시는 있지만, 기업들은 이미 17 이상으로 빠르게 넘어가는 중’이다. 한국 SI에서 이 문제가 더 강하게 느껴지는 이유는 따로 있다.
- ‘공공 SI와 장기 유지보수 계약’ — 한 번 만든 산출물이 오래 살아남는다.
- ‘전자정부프레임워크와 솔루션 호환성’ — 의존성 라인업이 묶여 있어 단독 업그레이드가 어렵다.
- ‘Oracle JDK 라이선스 변화의 오해’ — “Java는 8까지만 무료”로 굳어버린 인식.
- ‘JDK/JRE/JVM 구분의 흐릿함’ — 어디서 무엇을 올려야 하는지 사내에 합의가 없는 경우가 많다.
기술을 몰라서가 아니라 위 요소들이 한꺼번에 얽혀 있어서 미뤄진 것이다. 그래서 이 글은 기능 카탈로그 전에 그 매듭부터 풀고 시작한다.
잠깐 — JDK와 JRE는 뭐가 다른가
‘JRE’는 Java 애플리케이션을 실행하기 위한 런타임이다. JVM, 표준 라이브러리, 실행에 필요한 구성요소가 들어 있다.
‘JDK’는 개발 도구까지 포함한 묶음이다. JRE에 더해
javac컴파일러, 디버거, 모니터링 도구 같은 개발 도구가 함께 있다.서버에서 ‘Java 버전’을 확인할 때는 단순히
java -version만 볼 게 아니라 ‘빌드에 쓰는 JDK, 운영 컨테이너 이미지의 JDK/JRE, CI/CD 이미지의 JDK’가 각각 무엇인지 봐야 한다. 이 셋이 어긋나 있는 환경이 의외로 많다.
잠깐 — “Java는 8까지만 무료”라는 오해
현장에서 Java 버전 이야기를 하다 보면 자주 듣는 말이 있다. “Java는 8까지만 무료 아닌가요?”
정확히 말하면 Java 자체가 8까지만 무료인 건 아니다. 문제는 ‘Java 언어’가 아니라 ‘Oracle JDK 배포판의 라이선스’다.
OpenJDK 기반 배포판은 여러 벤더(Adoptium/Temurin, Amazon Corretto, Azul Zulu, Microsoft Build of OpenJDK 등)가 무료로 제공한다. Oracle JDK도 21/25 같은 LTS는 ‘NFTC(No-Fee Terms and Conditions)’ 라이선스 아래 ‘정해진 기간 동안’ 상업·운영 사용까지 무료다. 예를 들어 Oracle JDK 25 업데이트는 2028년 9월까지 NFTC로 제공될 예정으로 안내돼 있다. (Oracle JDK 21은 2026년 9월 이후 라이선스 전환이 예정돼 있다.)
다만 ‘Oracle JDK 8/11/17의 일부 업데이트’는 라이선스 조건이 다르고, NFTC도 영구 무료가 아니라 기간이 정해져 있어 조직 정책에 따라 확인이 필요하다.
그래서 실무에서 해야 할 질문은 “Java 몇 버전이 무료인가?”가 아니라 “우리는 어떤 ‘JDK 배포판’을 어떤 ‘라이선스’로 쓰고 있는가?”다.
잠깐 — Java LTS는 무엇이고 왜 중요한가
‘LTS(Long-Term Support)’는 Oracle과 주요 OpenJDK 배포자가 장기간 보안·버그 패치를 약속하는 버전이다. Java는 6개월마다 새 버전이 나오지만, 모든 버전이 길게 지원되진 않는다.
2026년 4월 기준 LTS 라인업은 ‘Java 8, 11, 17, 21, 25’. 운영 서비스는 보통 LTS 위에서 돌리는 게 안전하다. 다음 LTS는 ‘Java 29(2027년 9월 예정)’다.
즉 ‘Java 25(2025년 9월) → Java 29(2027년 9월)’ 사이의 약 2년이 LTS 안정 구간이다. 지금(2026년 4월) 기준으로 보면 다음 LTS까지 약 1년 반 남았다. Java 25를 올려두면 매번 6개월 단위로 따라잡지 않아도 된다.
결론 먼저: 누가 지금 봐야 하나
‘한 줄 요약: Java 17에 머문 팀은 21 또는 25 검토, Java 8/11은 17부터 단계적, 신규는 25 기본.’
상황별로 짧게 정리한다.
- ‘신규 프로젝트’: 특별한 제약이 없다면 ‘Java 25 LTS’를 기본 후보로 둔다. Spring Boot 4와 시기적 짝이 맞다.
- ‘Java 17 운영 서비스’: 21 또는 25 LTS 검토 시점. 도약 거리가 짧아 마이그레이션 비용이 가장 적다.
- ‘Java 11 운영 서비스’: 17로 먼저 올린 뒤 25로 가는 단계 전환이 안전하다. 프레임워크와 라이브러리까지 함께 움직이는 경우 한 번에 25까지 가면 원인 분리가 어려워질 수 있다.
- ‘Java 8 운영 서비스’: 가장 무거운 케이스. Java 8 → 17 → 25의 두 단계 로드맵이 일반적이다.
- ‘라이브러리·프레임워크 호환이 큰 환경’: JDK 버전보다 의존성 BOM부터 본다. Spring/Hibernate/Jackson 라인이 LTS와 짝을 맞춰 움직인다.
한 장 요약: Java 8에서 25로 오면서 바뀐 것들
‘한 줄 요약: 핵심은 네 가지 — record, sealed, pattern matching, virtual threads. 나머지는 보너스.’
자잘한 변화는 많지만, 실무에서 코드 톤을 바꾸는 큰 줄기는 이 정도다.
| 변화 | 도입 (안정 기준) | 무엇이 좋아지나 | 운영 영향 |
|---|---|---|---|
| record | Java 16 | 불변 데이터 클래스를 한 줄로 선언. boilerplate 제거 | DTO/값 객체 코드량 급감 |
| sealed class/interface | Java 17 | 상속 계층을 명시적으로 닫음. 타입 분기 안전성↑ | 도메인 모델·이벤트 설계 단단해짐 |
pattern matching (instanceof) | Java 16 | 타입 검사와 캐스팅을 한 줄로 | instanceof 캐스팅 boilerplate 제거 |
pattern matching (switch) + record patterns | Java 21 | 타입 분기와 분해(destructuring)를 한 표현식으로 | if-else 사슬 사라짐, 누락 컴파일 에러로 잡힘 |
| virtual threads | Java 21 | 플랫폼 스레드보다 낮은 비용으로 많은 동시 작업 처리 | I/O 바운드 서비스에서 thread-per-request 모델이 다시 현실적 선택지 |
| Garbage Collector 진화 | Java 11~25 | ZGC, G1, Generational ZGC 등 선택지 확장 | 큰 힙·낮은 지연 요구에 대응 |
| 시작 시간 / 배포 최적화 | Java 9~25 | AppCDS, JDK AOT 흐름, GraalVM Native 생태계 | 컨테이너·서버리스에서 콜드 스타트 개선 가능성 |
| 기타 편의 | Java 10~25 | var, text block, record patterns 등 | 가독성·표현력 |
이 글은 위 네 가지 ‘큰 줄기’에 집중한다. 나머지는 마지막에 짧게 짚는다.
1. record — 데이터 클래스가 한 줄이 된다
‘한 줄 요약: getter/equals/hashCode/toString을 자동으로 만들어주는 불변 클래스. DTO·값 객체에 가장 큰 효과.’
Java 8에서는
public final class User {
private final Long id;
private final String email;
private final String name;
public User(Long id, String email, String name) {
this.id = id;
this.email = email;
this.name = name;
}
public Long getId() { return id; }
public String getEmail() { return email; }
public String getName() { return name; }
@Override
public boolean equals(Object o) { /* ... */ }
@Override
public int hashCode() { /* ... */ }
@Override
public String toString() { /* ... */ }
}
Lombok @Value로 줄이는 패턴이 흔했다.
Java 25에서는
public record User(Long id, String email, String name) {}
이게 끝이다. 컴파일러가 final 필드, accessor(id(), email(), name()), equals/hashCode/toString을 자동 생성한다. 검증이 필요하면 ‘compact constructor’에 추가한다.
public record User(Long id, String email, String name) {
public User {
if (email == null || !email.contains("@"))
throw new IllegalArgumentException("invalid email");
}
}
운영 영향
- DTO·이벤트·값 객체 같은 ‘불변 데이터 묶음’은 record로 옮기면 코드량이 대폭 줄고 의도가 분명해진다.
- Lombok 의존을 줄일 수 있다.
@Value/@Data자리에 record가 들어간다. - Jackson, Hibernate, MapStruct 같은 라이브러리는 최신 라인이면 대부분 record 친화적이다. 단, 일부 옛 버전은 record 직렬화에 제한이 있을 수 있어 의존성 라인업 확인이 필요하다.
한계
- record는 기본 ‘final’이고 상속 불가. 가변(setter) 필요한 도메인은 부적합.
- JPA
@Entity는 보통 가변 객체로 만드는 관행이라 그대로 record로 못 옮긴다. ‘JPA Entity는 클래스, DTO/Projection은 record’가 일반 패턴.
2. sealed — 상속 계층을 명시적으로 닫기
‘한 줄 요약: “이 타입을 상속할 수 있는 건 정확히 이것들”을 컴파일러에 알려줘서, 나중에 새 타입이 추가될 때 누락을 막는다.’
무엇을 푸는가
도메인 모델에서 ‘Payment는 CardPayment, BankTransfer, Voucher 셋만 있다’처럼 가능한 변형이 정해진 경우가 많다. Java 8까지는 이걸 표현할 좋은 방법이 없었다. enum은 동작을 다 담기 어렵고, 일반 abstract class는 누구나 상속할 수 있어서 도메인 경계를 닫지 못한다.
Java 17에서는 (안정화 시점)
public sealed interface Payment
permits CardPayment, BankTransfer, Voucher {}
public record CardPayment(String cardNumber, int amount) implements Payment {}
public record BankTransfer(String bankCode, String accountNo, int amount) implements Payment {}
public record Voucher(String voucherCode, int amount) implements Payment {}
permits 절이 ‘Payment를 상속할 수 있는 건 이 셋뿐이다’를 컴파일러에 못 박는다.
왜 좋은가
switch나 instanceof로 분기할 때 ‘컴파일러가 모든 경우를 다뤘는지 검사’한다. 새 결제 타입(예: CryptoPayment)이 추가되면 분기 코드가 컴파일 에러를 내면서 누락 지점을 알려준다. enum + switch에 가까운 안전성을, 풍부한 데이터를 담은 클래스에 적용할 수 있다.
운영 영향
- 도메인 이벤트, 결제 수단, 알림 채널, 인증 방식 같은 ‘닫힌 변형 집합’을 명확히 표현.
- 새 타입 추가 시 ‘모든 분기 지점이 컴파일 에러로 떠오른다’ — 누락된 처리 포인트가 사라진다.
- 일반 abstract class를 그대로 sealed로 바꿀 때는 ‘permits 명시’와 ‘하위 클래스의 final/non-sealed/sealed 결정’이 같이 필요하다. 큰 코드베이스에서는 점진 적용이 안전하다.
3. pattern matching — 타입 분기와 분해를 한 표현식으로
‘한 줄 요약: instanceof + 캐스팅, switch + if-else 사슬을 짧고 안전하게 묶는 표현식.’
Java 8에서는
String describe(Object obj) {
if (obj instanceof String) {
String s = (String) obj;
return "string of length " + s.length();
} else if (obj instanceof Integer) {
Integer n = (Integer) obj;
return "int " + n;
} else {
return "other";
}
}
Java 25에서는 (record patterns 포함)
String describe(Object obj) {
return switch (obj) {
case String s -> "string of length " + s.length();
case Integer n -> "int " + n;
case Payment p -> describePayment(p);
case null -> "null";
default -> "other";
};
}
String describePayment(Payment p) {
return switch (p) {
case CardPayment(var cardNumber, var amount) ->
"card *" + cardNumber.substring(cardNumber.length() - 4) + " " + amount + "원";
case BankTransfer(var bankCode, var account, var amount) ->
"bank " + bankCode + " " + amount + "원";
case Voucher(var code, var amount) ->
"voucher " + code + " " + amount + "원";
};
}
‘sealed + record + pattern matching’이 합쳐지면 — Payment의 모든 변형을 한 switch에서 분해해 쓸 수 있고, 누락이 컴파일 에러로 잡힌다. default가 필요 없다.
운영 영향
instanceof캐스팅 boilerplate 감소.- ‘닫힌 도메인 분기’에서 누락 위험이 줄어든다.
- if-else 사슬이 길어지던 자리(이벤트 핸들링, 응답 매핑)가 가독성 위주로 정리된다.
주의
- pattern matching이 강력해질수록
switch문이 ‘비즈니스 분기 결정’에 가까워진다. 분기가 너무 많으면 그 자체가 폴리모피즘으로 풀어야 할 신호다. - record 분해(
(var a, var b))는 이름 기반이 아니라 ‘컴포넌트 순서’로 풀린다. record 컴포넌트 순서 변경이 호출자에게 깨지는 변경이라는 점을 기억해야 한다.
4. virtual threads — 가벼운 비용으로 많은 동시 작업
‘한 줄 요약: I/O 대기가 많은 서버에서는 reactive가 유일한 선택지가 아니게 된다.’
Java 8 시절 사고방식
스레드는 비싸다. 한 요청마다 OS 스레드 하나를 잡으면 수천 동시성에서 메모리·문맥 전환 비용이 폭발한다. 그래서 reactive(Project Reactor, RxJava) 같은 비동기 모델이 등장했다. 코드는 짧지만 가독성과 디버깅 부담이 컸다.
Java 21에서 안정화된 virtual threads
JVM이 가벼운 ‘virtual thread’를 제공한다. 플랫폼 스레드 하나가 다수의 virtual thread를 carrier로 굴리고, I/O로 막히면 carrier에서 떼어내 다른 virtual thread를 돌린다.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
// 한 요청마다 virtual thread 하나
var data = httpClient.send(...);
db.update(...);
return data;
});
});
}
코드는 ‘thread per request’의 익숙한 모양이다. 동시성 한계는 OS 스레드 수가 아니라 메모리·하드웨어·뒤단의 capacity에서 결정된다.
운영 영향
- I/O 바운드 서버(API 게이트웨이, 외부 서비스 호출이 많은 서비스, 대시보드 백엔드)에서 thread-per-request 모델이 다시 현실적인 선택지가 된다.
- reactive 학습 부담 없이도 비슷한 수준의 동시성이 가능. ‘읽기 쉬운 코드’가 다시 선택지가 된다.
- 단, ‘CPU 바운드 작업’에서는 효과가 크지 않다.
synchronized pinning과 JEP 491
Java 21 시점에는 synchronized 블록 안에서 blocking I/O를 할 때 ‘carrier pinning'(virtual thread가 carrier 스레드를 놓지 못하고 함께 묶이는 현상)이 주요 주의점이었다. ‘Java 24의 JEP 491’ 이후 이 문제는 크게 완화됐다 — synchronized 안에서도 virtual thread가 carrier를 놓을 수 있게 됐다.
다만 native call, 오래된 라이브러리, 일부 APM/트레이싱 도구 호환성은 여전히 별도 확인이 필요하다. Spring Boot 4 문서도 virtual threads를 위해 ‘Java 24 이상’을 강하게 권장한다.
주의
- DB 커넥션 풀, 외부 API 동시 호출 한도 등 ‘뒤단의 capacity’가 그대로면, virtual thread만 늘려도 효과가 안 난다. 동시성 한계는 시스템 전체에서 가장 좁은 곳에서 결정된다.
- 트레이싱·로깅 도구가 virtual thread를 정확히 따라가는지 별도 확인. 모던 라인은 대체로 지원하지만, 오래된 APM 어댑터는 그렇지 않을 수 있다.
그 외 짚어둘 만한 변화 (요약)
‘한 줄 요약: 큰 줄기 네 개 외에도, 일상적으로 닿는 작은 개선이 많다.’
- ‘
var지역 변수 추론’ (Java 10): 타입을 컴파일러가 추론. 가독성 향상이지만 명확성과 트레이드오프. - ‘text block’ (Java 15): 여러 줄 문자열 리터럴. SQL·JSON 리터럴이 깔끔해진다.
- ‘String Templates’: Java 21/22에서 preview였지만 ‘JDK 23에서 철회’됐다. Java 25 기준 운영 기능으로 볼 수 없다. 오히려 ‘preview 기능을 운영 코드에 성급히 쓰면 안 되는 사례’로 보는 편이 맞다.
- ‘record patterns + 향상된
switch‘ (Java 21~): 위에서 다룬 pattern matching의 핵심. - ‘GC 진화’: Generational ZGC가 Java 21에 들어오면서 큰 힙·낮은 지연 시나리오가 더 쉬워졌다.
- ‘시작 시간 / 배포 최적화’: AppCDS, JDK AOT 흐름(Java 25 기준 AOT Command-Line Ergonomics, AOT Method Profiling 등)이 컨테이너·서버리스에 영향. GraalVM Native는 별도 GraalVM 생태계이고, Project Leyden은 ‘지금 운영에서 바로 쓰는 기능’이라기보다 ‘JVM 시작 시간과 워밍업을 줄이려는 장기 흐름’으로 이해하는 편이 안전하다.
- ‘Foreign Function & Memory API’ (Java 22): JNI 대안. 일반 백엔드 영향은 적지만 네이티브 통합 팀에는 큼.
Java 8 → 25 마이그레이션, 어떻게 접근할까
‘한 줄 요약: 한 번에 가지 말고 17 또는 21을 중간 정거장으로. JDK만 올리지 말고 라이브러리 BOM을 함께 올린다.’
단계적 전환 권장 경로
| 출발 | 1차 목표 | 최종 목표 | 비고 |
|---|---|---|---|
| Java 8 | Java 17 | Java 25 | 가장 큰 점프. Jakarta EE 전환과 겹치는 경우가 많다 |
| Java 11 | Java 17 | Java 25 | 모듈 시스템 정착, record/sealed 진입 |
| Java 17 | Java 21 또는 25 | Java 25 | virtual threads / pattern matching 본격 활용 |
| Java 21 | Java 25 | Java 25 | 가장 가벼운 점프 |
한국 SI라면 한 가지 더 — 전자정부프레임워크
전자정부프레임워크도 최신 라인은 Java 17과 Spring 6 계열로 올라왔다. ‘2026년 3월 31일 공지된 v5.0.0(beta)’은 Spring Boot 3.5.6을 지원하고, 실행환경 오픈소스도 Spring Framework 5.3.37에서 6.2.11로 업그레이드했다. 런타임 GitHub README도 Spring Framework 6.2.11 기반·Java 17 지원을 명시한다. 그러니 ‘전자정부프레임워크는 아직도 Java 7이다’라고 말하면 현재 기준으로는 부정확하다.
다만 레거시 흔적은 여전히 보인다. 4.3 Getting Started 문서는 실행환경을 Spring 5.3.37 + JDK 8 이상으로 설명하고, 4.2 Boot Sample의 Gradle 전환 문서는 2026년 3월 수정된 페이지인데도 Spring Boot 2.7.12와 java.sourceCompatibility = JavaVersion.VERSION_1_8 예시를 담고 있다. 그리고 3.5~3.10 라인은 JDK 7~8 환경으로 정리돼 있다.
문제는 ‘프레임워크 자체’보다 ‘이미 만들어진 프로젝트가 얼마나 오래 유지되는가’에 가깝다. 현장에서는 ‘신규 산출물은 v5/Spring 6/Java 17 위에서 시작하고, 기존 3.x/4.x 프로젝트 유지보수는 그대로 두는’ 이중 운영이 흔하다. 마이그레이션 일정은 결국 ‘유지보수 계약 단위’로 잡힌다.
같이 봐야 하는 것들
- ‘Spring Boot/Framework 라인’: Boot 4.0.6은 ‘Java 17 이상, Java 26까지 호환’이며 ‘Java 25 LTS 조합이 자연스럽다’. virtual threads 사용 시 ‘Java 24 이상’ 권장. (시리즈 1편 참고.)
- ‘Hibernate/Jackson/Lombok 라인’: 메이저 버전마다 Java 호환 범위가 다르다. BOM에 맡기는 게 안전.
- ‘빌드 도구’: Maven 3.6.3 이상, Gradle 8.14 이상 또는 9.x. 빌드 이미지의 JDK도 같이.
- ‘CI/CD’: GitHub Actions, GitLab CI 빌드 이미지의 JDK 버전. 매트릭스 빌드로 Java 17/21/25 동시 점검.
- ‘GC/메모리 옵션’: Java 8에서 박아둔 옵션은 전수 검토. 자세한 건 아래 개발자 메모.
- ‘관측성·APM’: New Relic, Datadog, Pinpoint 등의 에이전트가 새 JDK·virtual threads를 지원하는 라인인지 확인.
시작 순서 (실무 권장)
- 별도 브랜치에서 시작.
- JDK만 먼저 올리고 빌드·테스트가 도는지 확인. (라이브러리 BOM이 한 번에 같이 올라가는 경우가 많다.)
- 컴파일 경고 → 에러 순으로 정리. deprecated API 잡는다.
- record/sealed/pattern matching 같은 모던 기능은 '가장 부담 적은 레이어(DTO, 도메인 이벤트)부터' 점진 적용. 한 번에 모든 코드를 모던 스타일로 바꿀 필요 없다.
- virtual threads는 기능 도입과 별도로 'Boot 라인'을 먼저 올린 뒤, 작은 모듈에서 측정해보고 확장.
- staging에서 부하·메모리·로그를 한 주 이상 관찰하고 운영 반영.
마치며: Legacy Escape 시리즈 2편
이 글은 시리즈 ‘Legacy Escape’의 2편이다. 1편(Spring Boot 4 업그레이드)에서 ‘프레임워크를 올리려면 결국 JDK도 봐야 한다’고 미뤄둔 부분을 이번에 풀었다.
요약하면:
- Java 8 고착은 한국만의 문제는 아니지만, 한국 SI에서 더 강하게 느껴진다. 공공 SI, 장기 유지보수, 전자정부프레임워크, Oracle 라이선스 오해, JDK/JRE 구분의 흐릿함이 한꺼번에 얽혀 있어서다.
- Java 25 LTS는 단순한 버전 업이 아니라 ‘record + sealed + pattern matching + virtual threads’가 합쳐져 코드 톤이 바뀌는 분기점이다.
- ‘신규는 25 기본, 17은 21 또는 25 검토, 11은 17 → 25, 8은 17 → 25 두 단계’가 현실적인 경로.
- JDK만 올리지 말고 ‘Spring Boot/Hibernate/Jackson/빌드 이미지/APM’을 같은 분기에 함께 본다.
- 모던 스타일 적용은 ‘한꺼번에’ 말고 ‘부담 적은 레이어부터 점진’으로.
다음 글은 ‘Spring AI 2026 업데이트’다. AI Decoded 외전으로, Spring Boot 4 / Java 25 시점에 맞춰 Spring AI 1.0/1.1/2.0-M 흐름을 정리한다. Boot 4 글에서 잠깐 언급한 Spring AI 통합을 한 단계 깊게 본다.
📦 개발자를 위한 추가 메모
record 도입 시 점검 포인트
- Jackson 직렬화:
jackson-databind최신 라인은 record 친화적. Boot 4가 끌고 오는 Jackson 3.x가 표준 경로. - Hibernate:
@Embeddable은 record 호환이 가능하지만@Entity는 보통 가변 객체로 둔다. JPA Projection·DTO 자리에는 record 적극 추천. - Lombok 동거: Lombok과 record는 공존 가능. 다만 record 자리에는 Lombok이 거의 필요 없어진다. 점진 정리 추천.
- 직렬화 커스터마이징:
@JsonProperty, custom serializer는 record에도 그대로 동작. accessor 이름이getName()이 아니라name()이라는 점만 라이브러리 호환에서 한 번 확인.
sealed 도입 시 점검 포인트
permits에 명시한 클래스는 같은 모듈/패키지 안에 있어야 한다(모듈러 환경 기준). 일부 케이스는 unnamed module에서도 동작하지만, 명시적 모듈화가 안전.- 하위 타입은
final,non-sealed,sealed중 하나를 골라야 한다. - ORM 매핑이 필요한 도메인이라면 sealed보다 별도 인터페이스 + 매핑 전략을 함께 설계.
pattern matching 도입 시 점검 포인트
instanceofpattern matching은 Java 16에서 안정.switchpattern matching과 record patterns(분해)는 Java 21에서 안정.case null명시적 처리. 기존switch는 null이면 NPE였지만, pattern switch는case null처리가 가능해 안전성을 올린다.- IDE 인텔리제이/이클립스 최신 라인이 자동 변환을 제공. 큰 코드베이스는 IDE 도움을 적극 활용.
virtual threads 도입 시 점검 포인트
- Spring Boot 3.2+ 기준
spring.threads.virtual.enabled=true한 줄로 Tomcat 요청 스레드를 virtual로 바꾼다. Boot 4에서도 동일 흐름이며, ‘Java 24 이상’이 강하게 권장된다(JEP 491 이후 pinning 완화). - DB 커넥션 풀(HikariCP) 크기는 그대로 두면 의미 없다. virtual thread 수와 풀 사이즈의 균형을 다시 측정.
synchronized블록 안에서 I/O를 하는 코드는 Java 21에서 carrier pinning을 일으킬 수 있었다. Java 24 JEP 491 이후 상당히 완화됐지만, native call·오래된 라이브러리·APM 호환성은 여전히 점검 필요.- 트레이싱: OpenTelemetry, Micrometer 최신 라인은 virtual thread 친화적. 오래된 APM 에이전트는 별도 확인.
GitHub Actions 매트릭스 예시
strategy:
matrix:
java-version: [ "17", "21", "25" ]
boot-version: [ "3.5.x", "4.0.6" ]
exclude:
- java-version: "17"
boot-version: "4.0.6" # 호환되지만 운영 권장은 21+
이렇게 두면 ‘Boot 3.5 + Java 17(현재 운영)’, ‘Boot 4 + Java 21(중간)’, ‘Boot 4 + Java 25(목표)’를 한 번에 검증할 수 있다.
JVM 시작 옵션 정리
Java 8 시절에 박아둔 JVM 옵션은 반드시 전수 검토한다.
- ‘CMS 관련 옵션’ (
-XX:+UseConcMarkSweepGC등): CMS GC는 JDK 14에서 제거됐다. 더 이상 정상 선택지가 아니다. - ‘GC 로그 옵션’: Java 9 이후 ‘Unified Logging’ (
-Xlog:...) 체계로 바뀌었다. 옛-XX:+PrintGCDetails같은 옵션은 새 형식으로 옮긴다. - ‘오래된 GC 조합’ (
-XX:+UseParallelOldGC등): deprecated 흐름. 일반적으로는-XX:+UseParallelGC나 G1(-XX:+UseG1GC, 기본)로 정리. - ‘GC 권장’: 일반 백엔드는 G1(기본). 큰 힙·낮은 지연이 필요하면 ‘Generational ZGC'(Java 21+).
- ‘메모리·스레드 옵션’ (
-Xms,-Xmx,-Xss): 제거 대상이 아니라 ‘현재 컨테이너·스레드 모델에 맞게 재검토할 대상’이다. - ‘컨테이너 한정’:
-XX:MaxRAMPercentage로 컨테이너 메모리 비율 지정. 고정-Xmx보다 유연.
모던 Java를 한 줄로
”Java 25는 새 기능을 외우는 글이 아니라, 코드 톤이 바뀌는 분기점이다. record로 줄이고, sealed로 닫고, pattern matching으로 분기를 안전하게, virtual threads로 동시성을 가볍게 — 네 가지가 한 세트다.”