Spring Boot 3.x 시대: 무엇이 달라졌나

Spring Boot 3.x는 Java 17 이상 필수, Jakarta EE 10 기반으로 전환하며 현대적인 Java 생태계와 발맞추고 있습니다. 2024년 11월 출시된 Spring Boot 3.4와 2025년 5월 출시된 Spring Boot 3.5는 Virtual Threads 완성도를 높이고, GraalVM Native 이미지 지원을 강화하며, 구조화 로깅(Structured Logging) 기능을 도입했습니다.

Virtual Threads 설정 및 활용

Spring Boot 3.2부터 Virtual Threads를 공식 지원합니다. 단 하나의 설정만으로 Tomcat, Jetty, 스케줄러, 비동기 작업이 모두 Virtual Thread에서 실행됩니다.

# application.properties
spring.threads.virtual.enabled=true

# application.yml
spring:
  threads:
    virtual:
      enabled: true
// Spring Boot 3.4+ Virtual Thread 동작 확인
@RestController
public class ThreadInfoController {
    
    @GetMapping("/thread-info")
    public Map getThreadInfo() {
        Thread currentThread = Thread.currentThread();
        return Map.of(
            "threadName", currentThread.getName(),
            "isVirtual", currentThread.isVirtual(),
            "threadId", currentThread.threadId()
        );
    }
}

// @Async 메서드도 Virtual Thread에서 실행
@Service
public class EmailService {
    
    @Async
    public CompletableFuture sendEmailAsync(String to, String subject) {
        // Virtual Thread에서 실행 (spring.threads.virtual.enabled=true 시)
        emailSender.send(to, subject);
        return CompletableFuture.completedFuture(null);
    }
}

// 스케줄러도 Virtual Thread 활용 (Spring Boot 3.4+)
@Component
public class ScheduledTasks {
    
    @Scheduled(fixedDelay = 5000)
    public void performTask() {
        // Spring Integration의 TaskScheduler가 Virtual Thread 사용
        System.out.println("Virtual: " + Thread.currentThread().isVirtual());
    }
}

GraalVM Native Image 지원

Spring Boot 3.5에서 GraalVM Native Image 빌드가 더욱 안정화되었습니다. 기존 JVM 애플리케이션 대비 시작 시간이 수십 배 빠르고 메모리 사용량도 크게 줄어듭니다.

# Maven으로 Native Image 빌드
./mvnw -Pnative native:compile

# Gradle로 Native Image 빌드
./gradlew nativeCompile

# Docker 컨테이너용 Native Image 빌드 (Buildpacks)
./mvnw spring-boot:build-image -Pnative

# 결과 실행 (JVM 없이 실행 가능)
./target/my-application
// Native Image를 위한 힌트 설정
@Configuration
@ImportRuntimeHints(MyRuntimeHints.class)
public class AppConfig {
    // ...
}

public class MyRuntimeHints implements RuntimeHintsRegistrar {
    
    @Override
    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
        // 리플렉션 힌트 등록
        hints.reflection()
            .registerType(MyDomainClass.class,
                MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
                MemberCategory.INVOKE_DECLARED_METHODS);
        
        // 리소스 힌트 등록
        hints.resources()
            .registerPattern("data/*.json");
        
        // 직렬화 힌트 등록
        hints.serialization()
            .registerType(MySerializableClass.class);
    }
}

// @RegisterReflectionForBinding 사용 (더 간편)
@RegisterReflectionForBinding({
    OrderRequest.class,
    OrderResponse.class
})
@RestController
public class OrderController {
    // ...
}

구조화 로깅 (Structured Logging)

Spring Boot 3.4에서 도입된 구조화 로깅은 JSON 형식으로 로그를 출력하여 ELK Stack, Grafana Loki 등 로그 수집 시스템과 쉽게 통합할 수 있습니다.

# JSON 형식 로깅 활성화 (application.properties)
logging.structured.format.console=ecs
# 또는
logging.structured.format.console=logstash
logging.structured.format.file=ecs
logging.file.name=logs/app.log

# 출력 예시 (ECS 형식)
# {"@timestamp":"2026-04-08T09:00:00.000Z","log.level":"INFO",
#  "message":"사용자 로그인 성공","service.name":"user-service",
#  "process.pid":12345,"log.logger":"c.e.UserService"}
// 구조화 로그에 커스텀 필드 추가
import org.slf4j.MDC;

@Component
public class RequestLoggingFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        
        HttpServletRequest httpReq = (HttpServletRequest) req;
        String requestId = httpReq.getHeader("X-Request-Id");
        String userId = SecurityContextHolder.getContext().getAuthentication().getName();
        
        try (
            MDC.MDCCloseable reqIdCtx = MDC.putCloseable("requestId", requestId);
            MDC.MDCCloseable userCtx = MDC.putCloseable("userId", userId)
        ) {
            chain.doFilter(req, res);
            // 모든 로그에 requestId, userId 자동 포함
        }
    }
}

Spring Security 6.x: 최신 보안 설정

Spring Boot 3.x와 함께 제공되는 Spring Security 6.x는 람다 DSL 방식으로 설정이 단순해졌습니다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
            )
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
                .anyRequest().authenticated()
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtConverter()))
            )
            .build();
    }
    
    @Bean
    public JwtAuthenticationConverter jwtConverter() {
        JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
        converter.setAuthoritiesClaimName("roles");
        converter.setAuthorityPrefix("ROLE_");
        
        JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
        jwtConverter.setJwtGrantedAuthoritiesConverter(converter);
        return jwtConverter;
    }
}

Spring Data JPA + Virtual Threads 최적화

@Service
@Transactional(readOnly = true)
public class UserService {
    
    private final UserRepository userRepository;
    private final OrderRepository orderRepository;
    
    // Virtual Thread + 구조화 동시성으로 병렬 조회
    public UserDashboard getUserDashboard(Long userId) throws Exception {
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            
            var userTask = scope.fork(() -> userRepository.findById(userId)
                .orElseThrow(() -> new UserNotFoundException(userId)));
            
            var ordersTask = scope.fork(() ->
                orderRepository.findTop10ByUserIdOrderByCreatedAtDesc(userId));
            
            var statsTask = scope.fork(() ->
                orderRepository.getOrderStatsByUserId(userId));
            
            scope.join().throwIfFailed();
            
            return new UserDashboard(
                userTask.get(),
                ordersTask.get(),
                statsTask.get()
            );
        }
    }
}

// Repository에서 Projection 활용 (N+1 방지)
@Query("""
    SELECT u.id as id, u.name as name, u.email as email,
           COUNT(o.id) as orderCount
    FROM User u
    LEFT JOIN Order o ON o.userId = u.id
    WHERE u.id = :userId
    GROUP BY u.id
    """)
UserSummaryProjection findUserSummaryById(@Param("userId") Long userId);

application.properties 주요 설정 모음

# Spring Boot 3.4/3.5 주요 설정

# Virtual Threads
spring.threads.virtual.enabled=true

# 구조화 로깅
logging.structured.format.console=ecs

# 데이터소스 설정
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000

# JPA 설정
spring.jpa.open-in-view=false
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.properties.hibernate.jdbc.batch_size=50
spring.jpa.properties.hibernate.order_inserts=true

# Actuator
management.endpoints.web.exposure.include=health,info,metrics,prometheus
management.endpoint.health.show-details=when-authorized

# 프로파일 활성화
spring.profiles.active=prod

정리

Spring Boot 3.4/3.5는 현대 Java 생태계의 최신 기능을 적극적으로 수용하고 있습니다. Virtual Threads 한 줄 설정으로 높은 동시성 처리가 가능해졌고, GraalVM Native Image로 컨테이너 환경에서의 시작 속도와 메모리 효율이 크게 개선되었습니다. 구조화 로깅과 개선된 Actuator는 프로덕션 환경에서의 관측 가능성을 높여줍니다. 신규 프로젝트라면 Spring Boot 3.4 이상을 선택하고, 특히 Virtual Threads 설정은 반드시 활성화하는 것을 권장합니다.