개요

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 이상 조합을 권장하며, 기존 프로젝트는 위 체크리스트를 참고해 단계적으로 업그레이드하세요.