Contents
see List개요
Spring Boot 3.4/3.5와 Spring Framework 7.0이 출시되면서 자바 생태계에 큰 변화가 찾아왔습니다. 구조적 로깅(Structured Logging), Virtual Threads 공식 지원, REST API 버전닝 내장, JSpecify 기반 null 안전성 어노테이션 등 실무에서 즉시 활용 가능한 기능들이 대거 추가됐습니다. 이 글에서는 각 기능의 개념부터 코드 예제까지 단계별로 설명합니다.
1. Spring Boot 3.4 구조적 로깅 (Structured Logging)
Spring Boot 3.4는 JSON 형태의 구조적 로그를 기본으로 지원합니다. ECS(Elastic Common Schema), Logstash, GELF 세 가지 포맷을 application.properties만으로 활성화할 수 있어 별도 라이브러리 없이 ELK 스택과 연동이 가능합니다.
1-1. 설정 방법
# application.properties
# 콘솔 출력을 JSON(Logstash 포맷)으로 변경
logging.structured.format.console=logstash
# 파일 출력은 ECS 포맷으로
logging.structured.format.file=ecs
logging.file.name=app.log
# application.yml (동일 설정)
logging:
structured:
format:
console: logstash
file: ecs
file:
name: app.log
1-2. 출력 예시 (Logstash 포맷)
{
"@timestamp": "2026-04-17T08:00:00.000Z",
"@version": "1",
"message": "User login successful",
"logger_name": "com.example.AuthService",
"thread_name": "http-nio-8080-exec-1",
"level": "INFO",
"level_value": 20000,
"userId": "u-12345",
"requestId": "req-abc"
}
1-3. 커스텀 필드 추가 (MDC 활용)
import org.slf4j.MDC;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
public class RequestIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String requestId = UUID.randomUUID().toString();
MDC.put("requestId", requestId);
try {
chain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}
2. Virtual Threads 설정 및 활용
Java 21에서 정식 출시된 Virtual Threads(가상 스레드)를 Spring Boot 3.2 이상에서 한 줄 설정으로 활성화할 수 있습니다. 가상 스레드는 수백만 개를 생성해도 OS 스레드처럼 자원을 소비하지 않아 I/O 집약적 애플리케이션의 처리량을 크게 향상시킵니다.
2-1. 활성화
# application.properties
spring.threads.virtual.enabled=true
이 설정 하나로 Tomcat의 요청 처리 스레드, @Async 작업, Spring MVC의 비동기 처리가 모두 가상 스레드로 전환됩니다. Spring Boot 3.4/3.5에서는 OtlpMeterRegistry, Undertow도 가상 스레드를 올바르게 지원합니다.
2-2. @Async와 Virtual Threads
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public Executor taskExecutor() {
// virtual threads를 사용하는 Executor
return Executors.newVirtualThreadPerTaskExecutor();
}
}
@Service
public class EmailService {
@Async
public CompletableFuture<Void> sendEmail(String to, String subject) {
// 이 메서드는 가상 스레드에서 실행됨
// I/O 블로킹이 발생해도 OS 스레드를 점유하지 않음
emailClient.send(to, subject);
return CompletableFuture.completedFuture(null);
}
}
2-3. 성능 고려사항
가상 스레드 사용 시 synchronized 블록에서 캐리어 스레드가 고정(pinning)되는 문제가 발생할 수 있습니다. Java 21에서는 ReentrantLock으로 대체하거나 JVM 옵션 -Djdk.tracePinnedThreads=full로 핀닝 발생 지점을 추적할 수 있습니다.
// synchronized 대신 ReentrantLock 사용
private final ReentrantLock lock = new ReentrantLock();
public void criticalSection() {
lock.lock();
try {
// 가상 스레드 핀닝 없이 안전하게 실행
} finally {
lock.unlock();
}
}
3. Spring Framework 7.0 내장 API 버전닝
Spring Framework 7.0(2025년 11월 출시)에서 REST API 버전닝이 first-class citizen으로 승격됐습니다. 기존에는 URL 경로 분기, 커스텀 필터, 별도 라이브러리를 통해 구현해야 했지만, 이제는 @GetMapping 등 매핑 어노테이션의 version 속성만으로 처리할 수 있습니다.
3-1. 의존성 (Spring Boot 4.0 기준)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>4.0.0</version>
</dependency>
3-2. API 버전 전략 설정
@Configuration
public class ApiVersionConfig implements WebMvcConfigurer {
@Override
public void configureApiVersioning(ApiVersioningConfigurer configurer) {
configurer
.useRequestHeader("X-API-Version") // 헤더 기반
// .useQueryParam("version") // 쿼리 파라미터 기반
// .usePathPrefix("/v{version}") // 경로 기반
.defaultVersion("1.0");
}
}
3-3. 컨트롤러에서 버전 선언
@RestController
@RequestMapping("/api/users")
public class UserController {
// v1.0 응답: 기본 사용자 정보
@GetMapping(path = "/{id}", version = "1.0")
public UserV1 getUserV1(@PathVariable Long id) {
return userService.findByIdV1(id);
}
// v1.1 이상: 추가 필드 포함
@GetMapping(path = "/{id}", version = "1.1")
public UserV2 getUserV2(@PathVariable Long id) {
return userService.findByIdV2(id);
}
// 특정 버전에서 deprecated 처리 (RFC 9745 준수)
@PostMapping(path = "/register", version = "1.0", deprecated = true)
public ResponseEntity<UserV1> registerV1(@RequestBody RegisterRequest req) {
return ResponseEntity.ok(userService.registerV1(req));
}
}
3-4. 클라이언트 요청 예시
# v1.0 요청
curl -H "X-API-Version: 1.0" https://api.example.com/api/users/42
# v1.1 요청
curl -H "X-API-Version: 1.1" https://api.example.com/api/users/42
4. JSpecify Null Safety 어노테이션
Spring Framework 7.0은 기존 JSR 305 기반 @NonNull, @Nullable을 JSpecify 표준으로 대체했습니다. 이를 통해 IDE와 정적 분석 도구가 null 관련 버그를 컴파일 시점에 더 정확히 잡아낼 수 있습니다.
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.jspecify.annotations.NullMarked;
@NullMarked // 패키지/클래스 단위로 non-null 기본값 선언
public class UserService {
// 반환값이 null일 수 있음을 명시
public @Nullable User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
// 파라미터와 반환값 모두 non-null (기본값)
public User createUser(@NonNull String name, @NonNull String email) {
return new User(name, email);
}
// 제네릭 타입의 null 가능성 명시 (기존 방식으로 불가능했던 표현)
public List<@Nullable String> findNullableNames() {
return repository.findAllNames();
}
}
5. Spring Boot 3.5 WebClient 글로벌 설정
Spring Boot 3.5에서 WebClient(리액티브 HTTP 클라이언트)도 RestClient/RestTemplate처럼 application.properties로 전역 설정이 가능해졌습니다.
# WebClient 전역 타임아웃 설정
spring.webclient.connect-timeout=5000
spring.webclient.read-timeout=10000
# 리다이렉트 자동 따라가기 (3.5부터 기본 활성화)
spring.webclient.follow-redirects=true
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient(WebClient.Builder builder) {
return builder
.baseUrl("https://api.example.com")
// 전역 설정이 자동으로 적용됨
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
}
6. Spring Boot 3.5 Heapdump 보안 강화
Spring Boot 3.5부터 heapdump Actuator 엔드포인트의 기본값이 access=NONE으로 변경됐습니다. 민감한 메모리 정보 유출을 방지하기 위한 조치로, 필요한 경우에만 명시적으로 활성화해야 합니다.
# heapdump 엔드포인트 명시적 활성화 (보안 주의)
management.endpoint.heapdump.access=unrestricted
management.endpoints.web.exposure.include=heapdump
# 운영 환경에서는 반드시 IP 제한 또는 인증 설정 필요
7. 마이그레이션 체크리스트
Spring Boot 3.x → 3.4/3.5 업그레이드 시 확인해야 할 주요 변경 사항입니다.
- Java 17 이상 필수: Spring Boot 3.x의 최소 JDK 요구사항은 Java 17입니다. Spring Framework 7/Boot 4로 넘어갈 경우 JDK 17 이상을 유지하되 JDK 25 기능도 활용 가능합니다.
- @ConfigurationProperties + @Validated: 3.4부터 중첩 프로퍼티 검증이 Bean Validation 스펙을 준수합니다. 중첩 객체에 @Valid를 붙이지 않으면 검증이 전파되지 않습니다.
- spring-boot-parent 모듈 제거: Spring Boot 3.5에서 spring-boot-parent 모듈 배포가 중단됐습니다. spring-boot-dependencies BOM을 직접 import하는 방식으로 전환하세요.
- Apache Pulsar 4.0 업그레이드: Spring Boot 3.5는 Pulsar 클라이언트를 3.3.x(EOL)에서 4.0.x(LTS)로 업그레이드했습니다. Pulsar 사용 프로젝트는 API 변경 사항을 검토해야 합니다.
<!-- spring-boot-parent 대신 BOM 직접 사용 예시 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.5.13</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
정리
Spring Boot 3.4/3.5와 Spring Framework 7.0은 실무에서 반복적으로 구현했던 구조적 로깅, API 버전닝, null 안전성 처리를 프레임워크 수준에서 표준화했습니다. Virtual Threads 활성화는 단 한 줄 설정으로 기존 코드를 건드리지 않고 처리량을 크게 높일 수 있습니다. 신규 프로젝트라면 Spring Boot 3.5(현 최신 LTS) + Java 21 이상 조합을 권장하며, 기존 프로젝트는 위 체크리스트를 참고해 단계적으로 업그레이드하세요.