Micronaut 5 Migration Guide — Java 25, Jackson 3, JSpecify와 Breaking Changes
Micronaut 5는 dependency version 하나만 올리는 upgrade가 아닙니다. Java baseline이 25로 바뀌고, Jackson Databind는 Jackson 3를 사용하며, nullability annotation은 JSpecify로 이동했습니다. RxJava 2와 MicroStream은 제거됐고 Security, Data, Views, Testcontainers에도 개별 breaking change가 있습니다.
이 글의 범위는 framework upgrade를 끝내고 기존 실행 모델로 회귀 테스트하는 단계까지입니다. Reactive 코드를 VT + blocking style로 바꾸는 작업은 upgrade가 안정된 뒤 다음 글에서 별도로 진행합니다.
이 글은 2026년 7월 26일 기준 Micronaut Core v5.0.0과 공식 Update to Micronaut 5를 기준으로 작성했습니다. module별 release가 따로 움직이므로 실제 upgrade에서는 사용하는 module의 breaking change도 함께 확인해야 합니다.
먼저 결정할 것
upgrade를 시작하기 전에 다음 세 질문에 답해야 합니다.
- 모든 build·test·운영 환경을 Java 25로 올릴 수 있는가?
- Jackson 2 API를 직접 사용하는 코드와 library가 모두 Jackson 3로 이동 가능한가?
- HTTP application 코드를 Reactive로 유지할 것인가, VT + blocking style로 옮길 것인가?
첫 번째가 불가능하면 Micronaut 5로 갈 수 없습니다. 두 번째가 불가능하면 Micronaut Serialization을 쓰는 경로와 Jackson Databind를 쓰는 경로를 분리해서 검토해야 합니다. 세 번째는 upgrade 성공 조건이 아니라 설계 선택입니다. Micronaut 5로 올린다고 기존 Reactor 코드가 자동으로 blocking 코드로 바뀌지는 않습니다.
제가 권하는 순서는 다음과 같습니다.
Java 25 / Gradle 9.5
→ Micronaut BOM / plugin 5.x
→ compile error 처리
→ module breaking change 처리
→ 기존 실행 모델 회귀 테스트
framework upgrade와 Reactive-to-blocking rewrite를 한 commit에서 동시에 진행하면 실패 원인을 분리하기 어렵습니다. 먼저 기존 동작을 Micronaut 5에서 복구하고, 그 다음 실행 모델을 바꾸는 편이 안전합니다.
1. Java와 build tool부터 올린다
Java 25는 선택이 아니다
Micronaut 4는 Java 17 이상을 지원했지만 Micronaut 5의 baseline은 Java 25입니다.1
Gradle toolchain을 사용한다면 다음처럼 맞춥니다.
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
Maven은 source와 target을 모두 25로 맞춥니다.
<properties>
<jdk.version>25</jdk.version>
<release.version>25</release.version>
</properties>
로컬 JDK만 바꾸고 끝내면 안 됩니다. 다음 환경이 모두 같은 bytecode와 runtime 조건을 사용해야 합니다.
- CI build와 test runner
- container base image
- deployment platform
- GraalVM Native Image toolchain
- IDE project SDK와 annotation processor
- integration test에서 띄우는 별도 JVM
Gradle application
공식 guide의 기준은 Micronaut Gradle Plugin 5.0.0과 Gradle 9.5입니다.
plugins {
id("io.micronaut.application") version "5.0.0"
id("com.gradleup.shadow") version "9.4.1"
}
gradle.properties에서 version을 직접 관리한다면 다음과 같이 바꿉니다.
micronautVersion=5.0.0
Kotlin application은 Kotlin 2.3 계열과 호환 KSP를 함께 올립니다.
plugins {
id("org.jetbrains.kotlin.jvm") version "2.3.21"
id("org.jetbrains.kotlin.plugin.allopen") version "2.3.21"
id("com.google.devtools.ksp") version "2.3.7"
}
plugin만 먼저 올렸을 때 Gradle 9에서 제거된 API를 사용하는 사내 plugin이 실패할 수 있습니다. 이 오류는 Micronaut compile error가 아니라 build infrastructure 문제이므로 먼저 분리해 처리합니다.
Maven application
<parent>
<groupId>io.micronaut.platform</groupId>
<artifactId>micronaut-parent</artifactId>
<version>5.0.0</version>
</parent>
<properties>
<micronaut.version>5.0.0</micronaut.version>
<jdk.version>25</jdk.version>
<release.version>25</release.version>
</properties>
BOM, parent, annotation processor에 서로 다른 Micronaut major version을 섞지 않는 것이 중요합니다. compile classpath는 5인데 processor가 4이면 generated bean definition과 runtime API가 어긋날 수 있습니다.
2. compile error를 성격별로 처리한다
JSpecify nullability
Micronaut 5는 자체 nullability annotation 대신 JSpecify를 사용합니다.2
// Micronaut 4
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
// Micronaut 5
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
단순 import 치환만으로 끝나지 않을 수 있습니다. JSpecify는 array, nested type, fully qualified type에서 annotation 위치를 더 엄격하게 해석합니다.
// 의도: list 자체는 non-null, element는 nullable
List<@Nullable String> names
Kotlin을 함께 쓰면 platform type 해석도 달라질 수 있습니다. Java compile 성공만 보지 말고 Kotlin caller에서 nullable return과 generic element type을 확인해야 합니다.
Jackson 3
micronaut-jackson-databind는 Micronaut 5에서 Jackson 3를 사용합니다.3
주요 core/databind package는 com.fasterxml.jackson.*에서 tools.jackson.*로 이동했습니다.
// Jackson 2
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
// Jackson 3
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.JsonNode;
다음 코드는 별도로 찾아야 합니다.
- custom
JsonSerializer와JsonDeserializer Moduleregistration- 직접 생성한
ObjectMapper JsonFactoryoption- Jackson annotation을 읽는 사내 library
- test fixture의
ObjectMapper
Micronaut의 JsonMapper 또는 Micronaut Serialization만 사용한 코드라면 직접 영향은 더 작습니다. 하지만 dependency tree에 Jackson 2와 3가 함께 들어오면 같은 class 이름처럼 보여도 type은 호환되지 않습니다. package migration이 끝난 뒤 runtime classpath를 확인해야 합니다.
3. module별 breaking change를 처리한다
Security annotation processor
@RolesAllowed, @PermitAll, @DenyAll을 쓴다면 processor를 바꿉니다.
// 제거
annotationProcessor("io.micronaut.security:micronaut-security-annotations")
// 추가
annotationProcessor("io.micronaut.security:micronaut-security-processor")
runtime dependency만 바꾸고 annotation processor를 빠뜨리면 compile-time security metadata가 생성되지 않습니다.
Micronaut Data embedded naming
embedded field의 column naming strategy가 바뀌었습니다. 기존 schema를 유지해야 하면 실제 column 이름을 명시합니다.
@Embeddable
public record TrainingPlanUserId(
@MappedProperty("id_training_plan_id")
TrainingPlan trainingPlan,
@MappedProperty("id_user_id")
User user
) {}
임시 호환 설정도 있습니다.
micronaut.data.embedded.naming.strategy=LEGACY
이 설정은 migration 시간을 벌기 위한 수단입니다. 장기적으로는 schema contract가 annotation에 드러나도록 @MappedProperty를 명시하는 편이 안전합니다. migration 전에 generated SQL과 실제 schema column을 비교해야 합니다.
Testcontainers artifact 이름
// 이전
testImplementation("org.testcontainers:junit-jupiter")
// 변경
testImplementation("org.testcontainers:testcontainers-junit-jupiter")
공식 guide의 artifact rename을 반영하지 않으면 dependency resolution 단계에서 실패합니다.
Views Turbo
Turbo integration은 별도 dependency로 이동했습니다.
implementation("io.micronaut.views:micronaut-views-turbo")
@TurboView는 @TurboStreamView로 이름이 바뀌었습니다.
EclipseStore와 제거된 module
EclipseStore annotation mapper도 processor module로 이동했습니다.
annotationProcessor("io.micronaut.eclipsestore:micronaut-eclipsestore-processor")
RxJava 2와 MicroStream은 Micronaut 5에서 제거됐습니다.4
- RxJava 2 → RxJava 3 또는 Reactor
- MicroStream → EclipseStore
Micronaut public API가 Reactive Streams를 중심으로 설계됐더라도 application 코드에서 io.reactivex.* type을 직접 노출했다면 package 변경과 operator 차이를 함께 처리해야 합니다.
4. 기존 실행 모델로 회귀 테스트한다
compile error가 사라졌다고 upgrade가 끝난 것은 아닙니다. 이 단계에서는 Reactive를 blocking으로 바꾸거나 thread-selection을 조정하지 않습니다. Micronaut 4에서 사용하던 실행 모델을 그대로 둔 채 Micronaut 5에서 같은 동작이 나오는지 먼저 확인합니다.
- Java 25에서 compile·unit test 성공
- generated bean definition과 annotation processor 정상 생성
- Jackson request·response snapshot 동일
- Security route 권한 동일
- Data embedded column과 generated SQL 동일
- 기존 Reactive integration test 성공
- 사용하는 module의 startup과 configuration binding 성공
framework upgrade와 실행 모델 전환을 한 변경에 섞으면 실패 원인이 Jackson 3인지, annotation processor인지, thread dispatch인지 분리하기 어렵습니다. 위 회귀 테스트가 통과한 commit을 먼저 기준점으로 남기는 편이 안전합니다.
framework migration 완료 조건
이 글에서 다루는 migration은 다음 네 조건을 만족하면 끝납니다.
- Java 25와 build plugin 변경이 CI, container, 운영 runtime까지 반영됐다.
- Jackson 3와 JSpecify compile·runtime 호환성을 확인했다.
- 실제 사용하는 module의 breaking change를 모두 처리했다.
- 기존 Reactive 실행 모델의 기능과 응답 contract가 Micronaut 5에서도 유지된다.
Reactive를 유지할지 VT + blocking으로 옮길지는 그 다음 설계 단계입니다. executor 선택, R2DBC→JDBC 전환, 동시성 손실, streaming API 경계는 다음 글에서 이어서 다룹니다.
다음 글: Micronaut 5와 Virtual Thread — Reactive에서 blocking으로 돌아갈 수 있을까
배경 시리즈: Micronaut 완전 가이드 5편 — HTTP 서버 모델과 Virtual Thread
Footnotes
댓글 영역에 가까워지면 자동으로 불러옵니다.
Preparing comments...