들어가며
Spring Boot 4.0이 2025년 11월에 정식 출시되었습니다. Spring Framework 7 기반으로 구축된 이번 메이저 버전은 Virtual Threads 기본 실행 모델, Spring AI 통합, JSpecify 기반 null safety 등 패러다임 수준의 변화를 포함합니다. 이 글에서는 Spring Boot 3.x에서 4.0으로 마이그레이션하는 과정을 단계별로 안내합니다.
1. Spring Boot 4.0 핵심 변경사항
| 항목 | Spring Boot 3.x | Spring Boot 4.0 |
|---|---|---|
| Spring Framework | 6.x | 7.x |
| 최소 Java 버전 | Java 17 | Java 21 |
| 스레드 모델 | Platform Threads (기본) | Virtual Threads (기본) |
| Null Safety | Spring @Nullable | JSpecify @Nullable |
| AI 통합 | 별도 프로젝트 | spring-boot-starter-ai 내장 |
| JAR 구조 | fat jar | 모듈화된 jar (CDS 최적화) |
| GraalVM | 실험적 | 1등급 지원 |
| Observability | Micrometer 1.x | Micrometer 2.x |
2. 사전 준비 - 3.x 최신 버전 업데이트
마이그레이션의 첫 단계는 Spring Boot 3.x 최신 버전(3.4.x)으로 먼저 업데이트하는 것입니다.
// build.gradle (3.x 최신 버전으로 먼저 업데이트)
plugins {
id 'org.springframework.boot' version '3.4.1'
id 'io.spring.dependency-management' version '1.1.7'
id 'java'
}
java {
sourceCompatibility = JavaVersion.VERSION_21 // Java 21로 전환
}
// 3.x에서 Deprecation 경고를 모두 해결한 후 4.0으로 올리세요
tasks.withType(JavaCompile).configureEach {
options.compilerArgs.add('-Xlint:deprecation')
}
체크리스트:
- Java 21 이상으로 전환 완료
- Deprecated API 사용 제거
- jakarta.* 네임스페이스 전환 확인 (3.0에서 이미 완료되었어야 함)
- 서드파티 라이브러리 호환성 확인
3. Virtual Threads 기본 실행 모델
Spring Boot 4.0의 가장 큰 변화는 Virtual Threads가 기본 실행 모델이 된 것입니다. 별도 설정 없이 Tomcat, Jetty, 그리고 @Async, @Scheduled 등 모든 스레드 관련 기능에서 Virtual Threads를 사용합니다.
Spring Boot 3.x에서의 Virtual Threads (명시적 활성화 필요)
# application.yml (Spring Boot 3.2+)
spring:
threads:
virtual:
enabled: true # 3.x에서는 명시적으로 활성화 필요
Spring Boot 4.0 (기본 활성화)
# application.yml (Spring Boot 4.0)
# Virtual Threads가 기본! 별도 설정 불필요
# Platform Threads로 되돌리려면:
spring:
threads:
virtual:
enabled: false # 명시적으로 비활성화할 때만
Virtual Threads 활용 예제
@RestController
@RequestMapping("/api/reports")
@RequiredArgsConstructor
public class ReportController {
private final ReportService reportService;
private final ExternalApiClient apiClient;
@GetMapping("/{id}")
public ResponseEntity<ReportResponse> getReport(@PathVariable Long id) {
// Virtual Thread에서 실행 - 블로킹 I/O 시에도 OS 스레드 점유하지 않음
Report report = reportService.findById(id);
ExternalData data = apiClient.fetchData(report.getExternalId());
return ResponseEntity.ok(
ReportResponse.of(report, data)
);
}
}
@Service
public class ReportService {
// StructuredTaskScope로 병렬 작업 관리 (Java 21+)
public AggregatedReport generateReport(Long userId) {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<UserProfile> profileTask =
scope.fork(() -> userClient.getProfile(userId));
Subtask<List<Order>> ordersTask =
scope.fork(() -> orderClient.getOrders(userId));
Subtask<CreditScore> creditTask =
scope.fork(() -> creditClient.getScore(userId));
scope.join();
scope.throwIfFailed();
return new AggregatedReport(
profileTask.get(),
ordersTask.get(),
creditTask.get()
);
} catch (Exception e) {
throw new ReportGenerationException("리포트 생성 실패", e);
}
}
}
Virtual Threads 마이그레이션 시 주의사항
- synchronized 블록 주의: Virtual Thread가 synchronized 블록 안에서 블로킹되면 캐리어 스레드(OS 스레드)를 고정(pin)합니다.
ReentrantLock으로 교체하세요. - ThreadLocal 사용 재검토: Virtual Threads에서 ThreadLocal은 메모리 오버헤드가 큽니다. ScopedValue 사용을 고려하세요.
- 커넥션 풀 크기 조정: Virtual Threads는 수만 개가 생성될 수 있어 DB 커넥션 풀이 병목됩니다. HikariCP maximumPoolSize를 적절히 설정하세요.
// Before: synchronized (Virtual Thread pinning 발생)
private synchronized void updateCache(String key, Object value) {
cache.put(key, value);
}
// After: ReentrantLock (pinning 방지)
private final ReentrantLock lock = new ReentrantLock();
private void updateCache(String key, Object value) {
lock.lock();
try {
cache.put(key, value);
} finally {
lock.unlock();
}
}
4. JSpecify Null Safety
Spring Framework 7은 기존 Spring의 @Nullable 어노테이션 대신 JSpecify 표준을 채택했습니다.
// Before: Spring @Nullable
import org.springframework.lang.Nullable;
import org.springframework.lang.NonNull;
public class UserService {
@Nullable
public User findByEmail(@NonNull String email) {
// ...
}
}
// After: JSpecify (Spring Boot 4.0)
import org.jspecify.annotations.Nullable;
import org.jspecify.annotations.NonNull;
// 패키지 수준에서 기본 NonNull 선언
// package-info.java
@NullMarked
package com.example.service;
import org.jspecify.annotations.NullMarked;
// 개별 클래스
public class UserService {
public @Nullable User findByEmail(String email) {
// @NullMarked 패키지이므로 email은 기본 NonNull
// ...
}
}
5. Spring AI 통합 (@AiClient)
Spring Boot 4.0은 AI 기능을 1등급으로 지원합니다. spring-boot-starter-ai를 추가하면 @AiClient를 통해 LLM을 선언적으로 호출할 수 있습니다.
// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-ai'
}
# application.yml
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o
temperature: 0.7
// @AiClient 선언적 AI 서비스
@AiClient
public interface ContentAssistant {
@UserMessage("다음 글의 요약을 3줄로 작성해줘: {{content}}")
String summarize(@Param("content") String content);
@UserMessage("다음 코드를 리뷰하고 개선 사항을 알려줘: {{code}}")
CodeReview reviewCode(@Param("code") String code);
}
public record CodeReview(
String summary,
List<String> improvements,
String refactoredCode
) {}
// 컨트롤러에서 사용
@RestController
@RequestMapping("/api/ai")
@RequiredArgsConstructor
public class AiController {
private final ContentAssistant contentAssistant;
@PostMapping("/summarize")
public ResponseEntity<Map<String, String>> summarize(
@RequestBody Map<String, String> request) {
String summary = contentAssistant.summarize(request.get("content"));
return ResponseEntity.ok(Map.of("summary", summary));
}
@PostMapping("/review")
public ResponseEntity<CodeReview> review(
@RequestBody Map<String, String> request) {
CodeReview review = contentAssistant.reviewCode(request.get("code"));
return ResponseEntity.ok(review);
}
}
6. 모듈화된 JAR 구조 및 CDS 최적화
Spring Boot 4.0은 빌드 결과물의 구조가 변경되어 Class Data Sharing(CDS)을 활용한 빠른 기동이 가능합니다.
# CDS 아카이브 생성 (최초 1회)
java -XX:ArchiveClassesAtExit=app-cds.jsa -Dspring.context.exit=onRefresh -jar myapp.jar
# CDS 아카이브를 활용한 빠른 기동
java -XX:SharedArchiveFile=app-cds.jsa -jar myapp.jar
# Docker 환경에서 활용
# Dockerfile
FROM eclipse-temurin:21-jre
COPY build/libs/myapp.jar /app/myapp.jar
# CDS 생성 레이어
RUN java -XX:ArchiveClassesAtExit=/app/app-cds.jsa \
-Dspring.context.exit=onRefresh -jar /app/myapp.jar
ENTRYPOINT ["java", "-XX:SharedArchiveFile=/app/app-cds.jsa", "-jar", "/app/myapp.jar"]
7. 단계별 마이그레이션 가이드
실제 프로젝트를 마이그레이션하는 순서입니다.
Step 1: build.gradle 업데이트
plugins {
id 'org.springframework.boot' version '4.0.0'
id 'io.spring.dependency-management' version '1.2.0'
id 'java'
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.jspecify:jspecify:1.0.0'
// Spring AI (필요시)
implementation 'org.springframework.boot:spring-boot-starter-ai'
}
Step 2: Deprecation 및 제거된 API 수정
// 제거된 API 예시
// Before (3.x에서 deprecated)
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers("/static/**");
}
// After (4.0 권장 방식)
@Bean
@Order(0)
public SecurityFilterChain staticResourceChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/static/**")
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
.securityContext(ctx -> ctx.disable())
.sessionManagement(session -> session.disable());
return http.build();
}
Step 3: synchronized -> ReentrantLock 교체
Virtual Threads의 pinning 이슈를 방지하기 위해 프로젝트 내 synchronized 키워드를 검색하여 교체합니다.
Step 4: Null Safety 어노테이션 마이그레이션
# IntelliJ IDEA에서 일괄 변환
# Edit > Find and Replace (Ctrl+Shift+R)
# org.springframework.lang.Nullable -> org.jspecify.annotations.Nullable
# org.springframework.lang.NonNull -> org.jspecify.annotations.NonNull
Step 5: 테스트 실행 및 검증
# 전체 테스트 실행
./gradlew test
# Virtual Threads pinning 감지 (JDK 옵션)
java -Djdk.tracePinnedThreads=short -jar myapp.jar
마치며
Spring Boot 4.0은 Java 21의 Virtual Threads를 기본값으로 채택하면서 동시성 처리의 패러다임을 바꿨습니다. 핵심 마이그레이션 포인트를 정리합니다.
- Java 21이 최소 요구 버전입니다. 먼저 JDK 업그레이드를 완료하세요.
- Virtual Threads가 기본이므로 synchronized, ThreadLocal 사용을 점검하세요.
- JSpecify 기반 null safety로 전환하면 컴파일 타임에 NPE를 방지할 수 있습니다.
- @AiClient로 LLM 통합이 선언적으로 가능해져, AI 기능 도입 장벽이 낮아졌습니다.
- CDS를 활용하면 애플리케이션 기동 시간을 크게 단축할 수 있습니다.
마이그레이션은 반드시 3.x 최신 버전에서 deprecation 경고를 모두 해결한 뒤 진행하세요. 급하게 버전을 올리기보다는 충분한 테스트와 함께 단계적으로 진행하는 것을 추천합니다.
'Spring Boot' 카테고리의 다른 글
| Spring Batch 실전 가이드 - 대용량 데이터 처리의 정석 (1) | 2026.04.12 |
|---|---|
| Spring Boot Actuator 완벽 활용 - 헬스체크부터 커스텀 메트릭까지 (0) | 2026.04.10 |
| Spring WebFlux 입문 - 리액티브 프로그래밍의 모든 것 (0) | 2026.04.02 |
| Spring Security 6 실전 가이드 - JWT + OAuth2 인증/인가 완벽 정리 (0) | 2026.04.02 |
| Spring AI 2.0과 Spring Boot 4 - AI 퍼스트 Java 개발의 시작 (0) | 2026.04.01 |