Skip to content

Split View: Spring Boot 3.x 완전 가이드 2025: Virtual Threads, GraalVM Native, Spring AI

✨ Learn with Quiz
|

Spring Boot 3.x 완전 가이드 2025: Virtual Threads, GraalVM Native, Spring AI

들어가며

Spring Boot 3.x는 Java 생태계에서 가장 강력한 프레임워크로 자리잡았습니다. Jakarta EE 10으로의 전환, Java 21 Virtual Threads 네이티브 지원, GraalVM Native Image 정식 지원, 그리고 Spring AI까지 — 2025년 현재 Spring Boot는 단순한 웹 프레임워크를 넘어 엔터프라이즈 AI 애플리케이션의 기반이 되고 있습니다.

이 가이드에서는 Spring Boot 3.x의 핵심 기능을 모두 다룹니다. Virtual Threads로 동시성을 극대화하는 방법부터 GraalVM으로 0.1초 기동을 달성하는 실전 설정, Spring AI로 LLM을 통합하는 파이프라인, 그리고 프로덕션 운영에 필요한 Observability, Security, 배포 전략까지 900줄 이상의 방대한 내용을 체계적으로 정리합니다.


1. Spring Boot 3.x 빅 픽처

1.1 Jakarta EE 마이그레이션

Spring Boot 3.0의 가장 큰 변화는 javax.* 에서 jakarta.* 네임스페이스로의 전환입니다.

// Before (Spring Boot 2.x)
import javax.persistence.Entity;
import javax.servlet.http.HttpServletRequest;

// After (Spring Boot 3.x)
import jakarta.persistence.Entity;
import jakarta.servlet.http.HttpServletRequest;

마이그레이션 체크리스트:

  • 모든 javax.persistencejakarta.persistence 로 변경
  • javax.servletjakarta.servlet 로 변경
  • javax.validationjakarta.validation 로 변경
  • 서드파티 라이브러리의 Jakarta EE 호환 버전 확인
  • OpenRewrite 자동 마이그레이션 레시피 활용
// build.gradle — OpenRewrite 자동 마이그레이션
plugins {
    id 'org.openrewrite.rewrite' version '6.16.0'
}

rewrite {
    activeRecipe(
        'org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_3'
    )
}

dependencies {
    rewrite platform('org.openrewrite.recipe:rewrite-recipe-bom:2.13.0')
    rewrite 'org.openrewrite.recipe:rewrite-spring'
}

1.2 Java 21 베이스라인

Spring Boot 3.2 이상은 Java 17이 최소 요구사항이며, 3.3부터는 Java 21을 적극 활용합니다.

기능Java 17Java 21
Record 패턴기본향상된 패턴 매칭
Sealed 클래스지원완전 지원
Virtual Threads미지원정식 지원
Structured Concurrency미지원Preview
String Templates미지원Preview

1.3 모듈 구조 권장 패턴

my-app/
  app-api/          # REST 컨트롤러, DTO
  app-domain/       # 엔티티, 서비스, 리포지토리 인터페이스
  app-infra/        # DB, 외부 API 구현체
  app-common/       # 공통 유틸, 예외
  app-batch/        # 배치 잡
// settings.gradle
rootProject.name = 'my-app'
include 'app-api', 'app-domain', 'app-infra', 'app-common', 'app-batch'

2. Java 21 Virtual Threads 실전 활용

2.1 Virtual Threads vs OS Threads

기존 Java의 스레드 모델은 OS 커널 스레드와 1:1 매핑됩니다. 하나의 OS 스레드가 약 1MB의 스택 메모리를 사용하므로, 4GB 힙에서 최대 4,000개 정도의 동시 요청만 처리 가능했습니다.

Virtual Threads는 JVM이 관리하는 경량 스레드로, 수 KB의 메모리만 사용합니다.

// 전통적 스레드 풀 (Spring Boot 2.x 기본)
// 최대 200개 스레드 = 200개 동시 요청
@Configuration
public class ThreadConfig {
    @Bean
    public TomcatProtocolHandlerCustomizer<?> protocolHandler() {
        return handler -> handler.setMaxThreads(200);
    }
}

// Virtual Threads (Spring Boot 3.2+)
// 수만 개의 동시 요청 처리 가능
// application.yml 한 줄이면 됨
# application.yml — Virtual Threads 활성화
spring:
  threads:
    virtual:
      enabled: true

이것 하나로 Tomcat의 모든 요청 처리가 Virtual Thread에서 수행됩니다.

2.2 벤치마크: 전통 스레드 vs Virtual Threads

@RestController
@RequestMapping("/api/benchmark")
public class BenchmarkController {

    @GetMapping("/blocking")
    public String blockingCall() throws InterruptedException {
        // DB 호출 시뮬레이션 (100ms 블로킹)
        Thread.sleep(100);
        return "response from " + Thread.currentThread();
    }
}

부하 테스트 결과 (k6, 1000 동시 사용자):

메트릭전통 스레드 (200)Virtual Threads
RPS1,8509,200
P95 레이턴시520ms108ms
P99 레이턴시1,200ms115ms
메모리 사용800MB350MB
에러율3.2%0%

2.3 Virtual Threads vs WebFlux

// WebFlux (리액티브 — 학습 곡선 높음)
@GetMapping("/reactive")
public Mono<User> getUser(@PathVariable Long id) {
    return userRepository.findById(id)
        .flatMap(user -> enrichmentService.enrich(user))
        .onErrorResume(e -> Mono.error(new UserNotFoundException(id)));
}

// Virtual Threads (동기 코드 그대로 — 학습 곡선 낮음)
@GetMapping("/virtual")
public User getUser(@PathVariable Long id) {
    User user = userRepository.findById(id)
        .orElseThrow(() -> new UserNotFoundException(id));
    return enrichmentService.enrich(user);
}

Virtual Threads를 선택해야 하는 경우:

  • I/O 바운드 작업이 대부분 (DB, HTTP, 파일)
  • 기존 Spring MVC 코드베이스
  • 팀의 리액티브 경험이 부족한 경우

WebFlux를 선택해야 하는 경우:

  • 스트리밍 데이터 처리 (SSE, WebSocket)
  • 논블로킹 백프레셔가 필요한 경우
  • 이미 리액티브 스택에 익숙한 팀

2.4 Virtual Threads 사용 시 주의사항

// 주의 1: synchronized 블록에서 carrier thread pinning 발생
// BAD — carrier thread가 고정되어 성능 저하
public synchronized void updateCache() {
    // 이 안에서 블로킹 I/O 하면 안 됨
    httpClient.send(request, bodyHandler);
}

// GOOD — ReentrantLock 사용
private final ReentrantLock lock = new ReentrantLock();
public void updateCache() {
    lock.lock();
    try {
        httpClient.send(request, bodyHandler);
    } finally {
        lock.unlock();
    }
}

// 주의 2: ThreadLocal 과도 사용 금지
// Virtual Thread는 수만 개 생성되므로 ThreadLocal 메모리 누수 위험
// ScopedValue(Preview) 사용 권장

// 주의 3: CPU 바운드 작업에는 부적합
// 피보나치 계산 같은 CPU 작업은 Virtual Thread가 이점 없음
# Virtual Thread 모니터링 JFR 이벤트 활성화
# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,threaddump

3. GraalVM Native Image

3.1 왜 Native Image인가?

기존 JVM 애플리케이션은 기동에 2-5초, 메모리 200MB 이상이 필요합니다. GraalVM Native Image는 AOT(Ahead-of-Time) 컴파일을 통해 네이티브 바이너리를 생성합니다.

메트릭JVMNative Image
기동 시간2.5초0.08초
메모리250MB80MB
패키지 크기18MB (JAR)65MB (바이너리)
Peak 처리량높음JVM 대비 80-90%
빌드 시간5초3-5분

3.2 Native Image 빌드 설정

// build.gradle
plugins {
    id 'org.graalvm.buildtools.native' version '0.10.1'
    id 'org.springframework.boot' version '3.3.0'
}

graalvmNative {
    binaries {
        main {
            buildArgs.addAll(
                '--initialize-at-build-time=org.slf4j',
                '-H:+ReportExceptionStackTraces',
                '--verbose'
            )
            jvmArgs.add('-Xmx8g') // 빌드 시 메모리
        }
    }
}
# Native Image 빌드
./gradlew nativeCompile

# 실행 (0.08초 기동!)
./build/native/nativeCompile/my-app

# Docker로 빌드
./gradlew bootBuildImage

3.3 Reflection Hints 설정

GraalVM은 AOT 컴파일 시 리플렉션을 정적으로 분석합니다. 동적 리플렉션은 힌트를 제공해야 합니다.

// RuntimeHintsRegistrar로 힌트 등록
@ImportRuntimeHints(MyRuntimeHints.class)
@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

public class MyRuntimeHints implements RuntimeHintsRegistrar {
    @Override
    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
        // 리플렉션 힌트
        hints.reflection()
            .registerType(UserDto.class,
                MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
                MemberCategory.INVOKE_DECLARED_METHODS);

        // 리소스 힌트
        hints.resources()
            .registerPattern("templates/*.html")
            .registerPattern("static/**");

        // 직렬화 힌트
        hints.serialization()
            .registerType(UserDto.class);
    }
}

3.4 Native Image 제한사항과 해결책

주요 제한사항:

  • 동적 프록시: @RegisterReflectionForBinding 사용
  • CGLIB 프록시: Spring AOT가 자동 처리
  • 리소스 번들: @ResourceHint 로 등록
  • JNI: 수동 힌트 필요
// DTO에 @RegisterReflectionForBinding 사용
@RegisterReflectionForBinding(UserDto.class)
@RestController
public class UserController {
    // UserDto가 Native Image에서 직렬화/역직렬화 가능
}

4. Spring AI — LLM 통합

4.1 Spring AI 개요

Spring AI는 Spring 생태계에서 AI/LLM을 통합하는 공식 프레임워크입니다. OpenAI, Anthropic, Ollama, Azure OpenAI 등 다양한 모델을 지원합니다.

// build.gradle
dependencies {
    implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter:1.0.0'
    // 또는 Anthropic
    // implementation 'org.springframework.ai:spring-ai-anthropic-spring-boot-starter:1.0.0'
}
# application.yml
spring:
  ai:
    openai:
      api-key: your-api-key-here
      chat:
        options:
          model: gpt-4o
          temperature: 0.7

4.2 ChatClient 기본 사용

@Service
public class AiService {
    private final ChatClient chatClient;

    public AiService(ChatClient.Builder builder) {
        this.chatClient = builder
            .defaultSystem("당신은 친절한 Java 개발 도우미입니다.")
            .build();
    }

    // 간단한 텍스트 생성
    public String chat(String userMessage) {
        return chatClient.prompt()
            .user(userMessage)
            .call()
            .content();
    }

    // 구조화된 응답 (JSON -> Java 객체)
    public CodeReview reviewCode(String code) {
        return chatClient.prompt()
            .user(u -> u.text("다음 코드를 리뷰해주세요: {code}")
                .param("code", code))
            .call()
            .entity(CodeReview.class);
    }

    // 스트리밍 응답
    public Flux<String> streamChat(String userMessage) {
        return chatClient.prompt()
            .user(userMessage)
            .stream()
            .content();
    }
}

public record CodeReview(
    int score,
    List<String> issues,
    List<String> suggestions,
    String summary
) {}

4.3 RAG 파이프라인 구축

@Configuration
public class RagConfig {

    @Bean
    public VectorStore vectorStore(EmbeddingModel embeddingModel) {
        return new PgVectorStore(
            jdbcTemplate, embeddingModel,
            PgVectorStore.PgVectorStoreConfig.builder()
                .withDimensions(1536)
                .withIndexType(PgVectorStore.PgIndexType.HNSW)
                .build()
        );
    }
}

@Service
public class RagService {
    private final ChatClient chatClient;
    private final VectorStore vectorStore;

    public RagService(ChatClient.Builder builder, VectorStore vectorStore) {
        this.vectorStore = vectorStore;
        this.chatClient = builder
            .defaultAdvisors(
                new QuestionAnswerAdvisor(vectorStore,
                    SearchRequest.defaults().withTopK(5))
            )
            .build();
    }

    // 문서 인덱싱
    public void indexDocuments(List<Document> documents) {
        // 텍스트 분할
        TokenTextSplitter splitter = new TokenTextSplitter(800, 200, 5, 10000, true);
        List<Document> chunks = splitter.apply(documents);

        // 벡터 저장소에 저장
        vectorStore.add(chunks);
    }

    // RAG 기반 질의
    public String queryWithContext(String question) {
        return chatClient.prompt()
            .user(question)
            .call()
            .content();
    }
}

4.4 Tool Calling (Function Calling)

@Service
public class WeatherService {

    @Tool("현재 날씨를 조회합니다")
    public WeatherInfo getCurrentWeather(
        @ToolParam("도시 이름") String city
    ) {
        // 실제 날씨 API 호출
        return weatherApiClient.getWeather(city);
    }
}

@RestController
public class AiController {
    private final ChatClient chatClient;
    private final WeatherService weatherService;

    @GetMapping("/ai/chat")
    public String chat(@RequestParam String message) {
        return chatClient.prompt()
            .user(message)
            .tools(weatherService)
            .call()
            .content();
    }
}
// "서울 날씨 어때?" -> AI가 자동으로 WeatherService.getCurrentWeather("서울") 호출

5. Spring Security 6

5.1 SecurityFilterChain 설정

Spring Security 6에서는 WebSecurityConfigurerAdapter 가 완전히 제거되었습니다.

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
            )
            .cors(cors -> cors.configurationSource(corsConfigSource()))
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**", "/actuator/health").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers("/api/**").authenticated()
                .anyRequest().denyAll()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtConverter()))
            )
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint(new Http403ForbiddenEntryPoint())
            )
            .build();
    }

    @Bean
    public CorsConfigurationSource corsConfigSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("https://myapp.com"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        config.setAllowedHeaders(List.of("*"));
        config.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", config);
        return source;
    }
}

5.2 OAuth2 Resource Server + JWT

# application.yml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com/realms/my-realm
          # 또는 jwk-set-uri
// JWT 클레임 -> Spring Security Authority 매핑
@Bean
public JwtAuthenticationConverter jwtConverter() {
    JwtGrantedAuthoritiesConverter grantedAuthorities =
        new JwtGrantedAuthoritiesConverter();
    grantedAuthorities.setAuthoritiesClaimName("roles");
    grantedAuthorities.setAuthorityPrefix("ROLE_");

    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(grantedAuthorities);
    return converter;
}

// Method Security
@Service
public class UserService {

    @PreAuthorize("hasRole('ADMIN') or #userId == authentication.name")
    public User getUser(String userId) {
        return userRepository.findById(userId).orElseThrow();
    }

    @PostAuthorize("returnObject.email == authentication.name")
    public UserProfile getProfile(String userId) {
        return profileRepository.findByUserId(userId);
    }
}

5.3 CORS, CSRF 전략

REST API의 경우 일반적으로 CSRF를 비활성화하고 CORS를 명시적으로 설정합니다.

// SPA + REST API 환경에서의 보안 설정
@Bean
public SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
    return http
        .securityMatcher("/api/**")
        .csrf(AbstractHttpConfigurer::disable) // JWT 사용 시 CSRF 불필요
        .cors(cors -> cors.configurationSource(corsConfigSource()))
        .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
        .build();
}

6. Spring Data JPA + R2DBC

6.1 Hibernate 6 + Spring Data 2025

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String name;

    @Column(unique = true, nullable = false)
    private String email;

    @Enumerated(EnumType.STRING)
    private UserStatus status;

    @CreationTimestamp
    private LocalDateTime createdAt;

    @UpdateTimestamp
    private LocalDateTime updatedAt;
}

// Spring Data JPA Repository
public interface UserRepository extends JpaRepository<User, Long> {

    // 메서드 이름 기반 쿼리
    Optional<User> findByEmail(String email);

    // JPQL
    @Query("SELECT u FROM User u WHERE u.status = :status AND u.createdAt > :since")
    List<User> findActiveUsersSince(
        @Param("status") UserStatus status,
        @Param("since") LocalDateTime since
    );

    // Native Query
    @Query(value = "SELECT * FROM users WHERE email LIKE %:domain", nativeQuery = true)
    List<User> findByEmailDomain(@Param("domain") String domain);

    // Projection
    @Query("SELECT u.name as name, u.email as email FROM User u WHERE u.id = :id")
    Optional<UserSummary> findSummaryById(@Param("id") Long id);
}

interface UserSummary {
    String getName();
    String getEmail();
}

6.2 QueryDSL 통합

// build.gradle
dependencies {
    implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
    annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta'
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api:3.1.0'
}
@Repository
@RequiredArgsConstructor
public class UserQueryRepository {
    private final JPAQueryFactory queryFactory;

    public Page<User> searchUsers(UserSearchCond cond, Pageable pageable) {
        QUser user = QUser.user;

        BooleanBuilder where = new BooleanBuilder();

        if (cond.getName() != null) {
            where.and(user.name.containsIgnoreCase(cond.getName()));
        }
        if (cond.getStatus() != null) {
            where.and(user.status.eq(cond.getStatus()));
        }
        if (cond.getFromDate() != null) {
            where.and(user.createdAt.goe(cond.getFromDate()));
        }

        List<User> content = queryFactory
            .selectFrom(user)
            .where(where)
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .orderBy(user.createdAt.desc())
            .fetch();

        long total = queryFactory
            .selectFrom(user)
            .where(where)
            .fetchCount();

        return new PageImpl<>(content, pageable, total);
    }
}

6.3 R2DBC (리액티브 데이터 접근)

// R2DBC Repository (WebFlux 사용 시)
public interface ReactiveUserRepository extends ReactiveCrudRepository<User, Long> {
    Flux<User> findByStatus(UserStatus status);
    Mono<User> findByEmail(String email);
}

@Service
@RequiredArgsConstructor
public class ReactiveUserService {
    private final ReactiveUserRepository userRepository;

    public Flux<User> getActiveUsers() {
        return userRepository.findByStatus(UserStatus.ACTIVE)
            .delayElements(Duration.ofMillis(10)) // 백프레셔 시뮬레이션
            .onErrorResume(e -> Flux.empty());
    }
}

7. Testcontainers + @ServiceConnection

7.1 Testcontainers 기본 설정

// build.gradle
dependencies {
    testImplementation 'org.springframework.boot:spring-boot-testcontainers'
    testImplementation 'org.testcontainers:junit-jupiter'
    testImplementation 'org.testcontainers:postgresql'
    testImplementation 'org.testcontainers:kafka'
}

7.2 @ServiceConnection 자동 설정

Spring Boot 3.1부터 @ServiceConnection으로 Testcontainers를 자동 설정할 수 있습니다.

@SpringBootTest
@Testcontainers
class UserServiceIntegrationTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");

    @Container
    @ServiceConnection
    static KafkaContainer kafka = new KafkaContainer(
        DockerImageName.parse("confluentinc/cp-kafka:7.6.0")
    );

    @Autowired
    private UserService userService;

    @Autowired
    private UserRepository userRepository;

    @Test
    void shouldCreateUser() {
        // Given
        CreateUserRequest request = new CreateUserRequest("John", "john@test.com");

        // When
        User created = userService.createUser(request);

        // Then
        assertThat(created.getId()).isNotNull();
        assertThat(created.getName()).isEqualTo("John");

        User found = userRepository.findById(created.getId()).orElseThrow();
        assertThat(found.getEmail()).isEqualTo("john@test.com");
    }
}

7.3 테스트 격리와 CI/CD 통합

// 공통 테스트 설정 — 모든 통합 테스트에서 재사용
@TestConfiguration(proxyBeanMethods = false)
public class TestContainersConfig {

    @Bean
    @ServiceConnection
    public PostgreSQLContainer<?> postgresContainer() {
        return new PostgreSQLContainer<>("postgres:16-alpine")
            .withReuse(true); // 컨테이너 재사용으로 테스트 속도 향상
    }

    @Bean
    @ServiceConnection
    public GenericContainer<?> redisContainer() {
        return new GenericContainer<>("redis:7-alpine")
            .withExposedPorts(6379)
            .withReuse(true);
    }
}

// CI/CD에서의 Testcontainers (GitHub Actions)
# .github/workflows/test.yml
name: Integration Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
      - name: Run tests
        run: ./gradlew test
        env:
          TESTCONTAINERS_RYUK_DISABLED: false

8. Observability (Micrometer + OTLP)

8.1 Micrometer + OpenTelemetry 통합

// build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'io.micrometer:micrometer-registry-otlp'
    implementation 'io.micrometer:micrometer-tracing-bridge-otel'
    implementation 'io.opentelemetry:opentelemetry-exporter-otlp'
}
# application.yml
management:
  otlp:
    metrics:
      export:
        url: http://otel-collector:4318/v1/metrics
        step: 30s
    tracing:
      endpoint: http://otel-collector:4318/v1/traces
  tracing:
    sampling:
      probability: 1.0  # 프로덕션에서는 0.1 등으로 조절
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus,loggers
  metrics:
    tags:
      application: my-app
      environment: production

8.2 커스텀 메트릭

@Service
@RequiredArgsConstructor
public class OrderService {
    private final MeterRegistry meterRegistry;
    private final Counter orderCounter;
    private final Timer orderProcessingTimer;

    public OrderService(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.orderCounter = Counter.builder("orders.created")
            .tag("type", "standard")
            .description("Number of orders created")
            .register(meterRegistry);
        this.orderProcessingTimer = Timer.builder("orders.processing.time")
            .description("Order processing time")
            .register(meterRegistry);
    }

    public Order createOrder(CreateOrderRequest request) {
        return orderProcessingTimer.record(() -> {
            Order order = processOrder(request);
            orderCounter.increment();

            // 게이지 — 현재 상태
            meterRegistry.gauge("orders.pending",
                orderRepository.countByStatus(OrderStatus.PENDING));

            return order;
        });
    }
}

8.3 분산 추적 (Distributed Tracing)

@Service
@RequiredArgsConstructor
public class PaymentService {
    private final RestClient restClient;
    private final ObservationRegistry observationRegistry;

    // Spring Boot 3.x에서는 RestClient/WebClient가 자동으로 trace 전파
    public PaymentResult processPayment(PaymentRequest request) {
        Observation observation = Observation.createNotStarted(
            "payment.process", observationRegistry
        );

        return observation.observe(() -> {
            // 이 호출의 trace-id가 외부 서비스까지 전파됨
            return restClient.post()
                .uri("https://payment-gateway.com/api/charge")
                .body(request)
                .retrieve()
                .body(PaymentResult.class);
        });
    }
}

8.4 Grafana 대시보드 설정

# docker-compose.yml (Observability Stack)
version: '3.8'
services:
  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.96.0
    ports:
      - "4317:4317"   # gRPC
      - "4318:4318"   # HTTP
    volumes:
      - ./otel-config.yaml:/etc/otelcol/config.yaml

  prometheus:
    image: prom/prometheus:v2.51.0
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml

  grafana:
    image: grafana/grafana:10.4.0
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin

  jaeger:
    image: jaegertracing/all-in-one:1.55
    ports:
      - "16686:16686"  # UI
      - "14250:14250"  # gRPC

9. Docker + Kubernetes 배포

9.1 멀티 스테이지 Dockerfile

# Stage 1: Build
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY gradle/ gradle/
COPY gradlew build.gradle settings.gradle ./
RUN ./gradlew dependencies --no-daemon
COPY src/ src/
RUN ./gradlew bootJar --no-daemon -x test

# Stage 2: Runtime
FROM eclipse-temurin:21-jre-alpine
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring

COPY --from=builder /app/build/libs/*.jar app.jar

EXPOSE 8080
ENTRYPOINT ["java", \
  "-XX:+UseZGC", \
  "-XX:MaxRAMPercentage=75.0", \
  "-Djava.security.egd=file:/dev/./urandom", \
  "-jar", "app.jar"]

9.2 Cloud Native Buildpacks

# Buildpacks로 이미지 빌드 (Dockerfile 불필요)
./gradlew bootBuildImage --imageName=myapp:latest

# Native Image로 빌드
./gradlew bootBuildImage --imageName=myapp:native \
  -Pnative=true

9.3 Kubernetes 매니페스트

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: my-app
          image: myapp:latest
          ports:
            - containerPort: 8080
          env:
            - name: SPRING_PROFILES_ACTIVE
              value: "production"
            - name: JAVA_TOOL_OPTIONS
              value: "-XX:+UseZGC -XX:MaxRAMPercentage=75.0"
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "1000m"
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10
          startupProbe:
            httpGet:
              path: /actuator/health
              port: 8080
            failureThreshold: 30
            periodSeconds: 2
      terminationGracePeriodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
  name: my-app-service
spec:
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 8080
  type: ClusterIP

9.4 Graceful Shutdown

# application.yml
server:
  shutdown: graceful

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s
// 커스텀 Graceful Shutdown 로직
@Component
public class GracefulShutdownHandler implements DisposableBean {

    private final ExecutorService executorService;

    @Override
    public void destroy() throws Exception {
        log.info("Graceful shutdown initiated...");
        executorService.shutdown();
        if (!executorService.awaitTermination(25, TimeUnit.SECONDS)) {
            executorService.shutdownNow();
        }
        log.info("Graceful shutdown completed");
    }
}

10. 프로덕션 체크리스트 15

보안

  1. Security HeadersX-Content-Type-Options, X-Frame-Options, Content-Security-Policy 설정
  2. Actuator 보안/actuator 엔드포인트 인증 필수, 민감 정보 비활성화
  3. Secrets 관리 — 환경변수 또는 Vault로 시크릿 주입, application.yml에 하드코딩 금지
  4. HTTPS 강제server.ssl.enabled=true 또는 로드밸런서 TLS 종단
  5. 의존성 취약점 스캔 — Dependabot, Snyk, OWASP Dependency Check

성능

  1. Connection Pool — HikariCP 설정 최적화 (max-pool-size, connection-timeout)
  2. 캐시 전략 — Spring Cache + Redis/Caffeine 2레벨 캐시
  3. JVM 튜닝 — ZGC/G1GC 선택, MaxRAMPercentage 설정

안정성

  1. Health Check — Readiness/Liveness Probe 분리
  2. Circuit Breaker — Resilience4j로 외부 서비스 장애 격리
  3. Rate Limiting — Bucket4j 또는 API Gateway에서 처리

운영

  1. Profile 관리 — local, dev, staging, production 환경 분리
  2. 로그 구조화 — JSON 로그 포맷, MDC로 요청 추적
  3. DB 마이그레이션 — Flyway/Liquibase 필수
  4. Graceful Shutdown — 무중단 배포를 위한 설정
# 프로덕션 application-production.yml 예시
server:
  shutdown: graceful
  port: 8080

spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 3000
      idle-timeout: 600000
      max-lifetime: 1800000

  cache:
    type: redis
    redis:
      time-to-live: 600000

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: when-authorized
      probes:
        enabled: true

logging:
  pattern:
    console: '{"timestamp":"%d","level":"%p","logger":"%c","message":"%m","trace":"%X{traceId}","span":"%X{spanId}"}%n'

11. 인터뷰 질문 15선

기본 (1-5)

Q1. Spring Boot 3.x에서 가장 큰 변화는 무엇인가요?

Jakarta EE 10 마이그레이션(javax에서 jakarta 네임스페이스), Java 17+ 필수, Spring Security 6 통합, Observability 개선(Micrometer Tracing), GraalVM Native Image 정식 지원이 핵심 변화입니다.

Q2. Virtual Threads와 기존 스레드 풀의 차이를 설명하세요.

기존 스레드는 OS 커널 스레드와 1:1 매핑되어 약 1MB의 스택 메모리를 사용합니다. Virtual Threads는 JVM이 관리하는 경량 스레드로 수 KB만 사용하며, M:N 스케줄링으로 수만 개의 동시 요청을 처리합니다. Spring Boot 3.2에서 spring.threads.virtual.enabled=true로 활성화합니다.

Q3. @SpringBootApplication은 어떤 애노테이션의 조합인가요?

@SpringBootConfiguration (설정 클래스), @EnableAutoConfiguration (자동 설정), @ComponentScan (컴포넌트 스캔)의 조합입니다.

Q4. Spring Boot의 Auto-Configuration이 동작하는 원리를 설명하세요.

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일에 등록된 설정 클래스들이 조건부(@ConditionalOnClass, @ConditionalOnMissingBean 등)로 평가되어 필요한 빈만 자동 등록됩니다.

Q5. Spring Boot 3.x에서 기존 javax를 jakarta로 마이그레이션하는 이유는?

Oracle이 Java EE를 Eclipse Foundation에 이관하면서 javax 네임스페이스 사용권을 이전하지 않았기 때문입니다. Jakarta EE는 독립적인 진화를 위해 새 네임스페이스를 채택했습니다.

심화 (6-10)

Q6. GraalVM Native Image의 장단점과 적합한 사용 사례는?

장점: 0.1초 기동, 50% 메모리 절약, 서버리스/CLI에 적합. 단점: 빌드 시간 3-5분, 리플렉션 제한, Peak 처리량 감소, 디버깅 어려움. 서버리스(AWS Lambda, Cloud Run), CLI 도구, 마이크로서비스에 적합합니다.

Q7. Spring Security 6에서 WebSecurityConfigurerAdapter 대신 어떤 방식을 사용하나요?

SecurityFilterChain 빈을 직접 등록합니다. HttpSecurity를 주입받아 Lambda DSL로 설정하고 http.build()로 반환합니다. @EnableMethodSecurity로 메서드 레벨 보안도 설정합니다.

Q8. Testcontainers에서 @ServiceConnection의 역할은?

Spring Boot 3.1에서 도입된 기능으로, Testcontainers의 컨테이너 정보(호스트, 포트, 인증)를 자동으로 application.properties에 매핑합니다. @DynamicPropertySource를 수동으로 설정할 필요가 없습니다.

Q9. Virtual Threads에서 synchronized 블록이 문제가 되는 이유는?

Virtual Thread가 synchronized 블록 안에서 블로킹 I/O를 수행하면 carrier thread(OS 스레드)가 고정(pinning)됩니다. 이로 인해 다른 Virtual Thread의 스케줄링이 차단되어 성능이 저하됩니다. ReentrantLock을 사용하면 해결됩니다.

Q10. Spring AI에서 RAG 파이프라인의 구성 요소를 설명하세요.

Document Loader(문서 로딩), Text Splitter(청크 분할), Embedding Model(벡터화), Vector Store(벡터 저장소), Retriever(유사도 검색), ChatClient + Advisor(컨텍스트 주입 후 LLM 질의)로 구성됩니다.

실전 (11-15)

Q11. Spring Boot에서 Graceful Shutdown을 구현하는 방법은?

server.shutdown=gracefulspring.lifecycle.timeout-per-shutdown-phase=30s를 설정합니다. SIGTERM 수신 시 새 요청을 거부하고 진행 중인 요청이 완료될 때까지 대기합니다. K8s에서는 terminationGracePeriodSeconds와 맞춰야 합니다.

Q12. Micrometer와 OpenTelemetry의 관계를 설명하세요.

Micrometer는 메트릭 수집의 추상화 레이어(SLF4J의 메트릭 버전)이고, OpenTelemetry는 메트릭+트레이싱+로깅의 벤더 중립적 표준입니다. Spring Boot 3.x에서는 Micrometer Tracing Bridge를 통해 OpenTelemetry 프로토콜(OTLP)로 데이터를 내보냅니다.

Q13. HikariCP 커넥션 풀 튜닝 시 주요 파라미터와 권장값은?

maximum-pool-size는 CPU 코어 수의 2배(보통 10-20), minimum-idle은 maximum-pool-size와 같게(풀 예열), connection-timeout은 3초(빠른 실패), max-lifetime은 30분(DB 커넥션 타임아웃보다 짧게), idle-timeout은 10분으로 설정합니다.

Q14. Spring Boot 프로파일을 효과적으로 관리하는 전략은?

application.yml(공통), application-local.yml, application-dev.yml, application-prod.yml로 분리합니다. 민감 정보는 환경변수나 Vault로 주입하고, spring.profiles.active는 환경변수로 설정합니다. Spring Cloud Config Server로 중앙 집중 관리도 가능합니다.

Q15. Circuit Breaker 패턴을 Spring Boot에서 구현하는 방법은?

Resilience4j를 사용합니다. @CircuitBreaker(name="myService", fallbackMethod="fallback")로 선언적으로 설정하고, application.yml에서 실패율 임계값, 대기 시간, 링 버퍼 크기를 설정합니다. Half-Open 상태에서 점진적으로 요청을 허용하여 서비스 회복을 감지합니다.


12. 퀴즈

Q1. Spring Boot 3.2에서 Virtual Threads를 활성화하는 설정은?

spring.threads.virtual.enabled=true 를 application.yml에 추가합니다. 이것으로 Tomcat의 모든 요청 처리가 Virtual Thread에서 수행됩니다.

Q2. GraalVM Native Image에서 리플렉션을 사용하려면 어떻게 해야 하나요?

RuntimeHintsRegistrar 를 구현하여 @ImportRuntimeHints 로 등록하거나, @RegisterReflectionForBinding 애노테이션을 사용합니다. Spring AOT가 빌드 시 자동으로 대부분의 힌트를 생성합니다.

Q3. Spring Security 6에서 CSRF를 비활성화하는 Lambda DSL 코드는?

http.csrf(AbstractHttpConfigurer::disable) 를 사용합니다. JWT 기반 Stateless API에서는 CSRF 보호가 불필요하므로 비활성화하는 것이 일반적입니다.

Q4. Testcontainers에서 @ServiceConnection이 @DynamicPropertySource보다 좋은 이유는?

@ServiceConnection 은 컨테이너 타입을 자동 인식하여 Spring Boot의 auto-configuration에 필요한 프로퍼티를 자동 매핑합니다. @DynamicPropertySource 는 수동으로 프로퍼티 이름과 값을 지정해야 합니다.

Q5. Spring AI에서 Tool Calling의 동작 원리는?

LLM이 사용자 질문을 분석하여 등록된 Tool 중 적합한 것을 선택하고, 파라미터를 추출합니다. Spring AI가 해당 Java 메서드를 실행하고 결과를 LLM에 전달하면, LLM이 최종 응답을 생성합니다. @Tool 애노테이션으로 메서드를 등록합니다.


참고 자료

  1. Spring Boot 공식 문서
  2. Spring AI 공식 문서
  3. Java 21 Virtual Threads JEP 444
  4. GraalVM Native Image 가이드
  5. Spring Security 6 마이그레이션 가이드
  6. Testcontainers Spring Boot
  7. Micrometer OpenTelemetry
  8. Spring Boot Docker 가이드
  9. Resilience4j Spring Boot
  10. HikariCP 설정 가이드
  11. Spring Data JPA 공식 문서
  12. QueryDSL JPA 가이드
  13. Kubernetes Spring Boot 배포
  14. OpenRewrite Spring Boot 3 마이그레이션
  15. Spring Boot Actuator 가이드
  16. Flyway DB 마이그레이션
  17. Spring Cloud Config
  18. Baeldung Spring Boot 3 가이드

Spring Boot 3.x Complete Guide 2025: Virtual Threads, GraalVM Native, Spring AI


Introduction

Spring Boot 3.x represents the most significant evolution in the Java ecosystem. With the Jakarta EE 10 migration, Java 17+ baseline, and Java 21 Virtual Threads native integration, it sets a new standard for enterprise backend development.

This guide covers everything you need to build production-ready applications with Spring Boot 3.x in 2025 — from Virtual Threads and GraalVM Native Image to Spring AI, Security 6, Testcontainers, and full observability.


1. Spring Boot 3.x Big Picture

1.1 Jakarta EE Migration

The most breaking change in Spring Boot 3.x is the namespace migration from javax to jakarta.

ComponentBefore (2.x)After (3.x)
Servletjavax.servletjakarta.servlet
JPAjavax.persistencejakarta.persistence
Validationjavax.validationjakarta.validation
Mailjavax.mailjakarta.mail
Injectjavax.injectjakarta.inject
// Before: Spring Boot 2.x
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.validation.constraints.NotNull;

// After: Spring Boot 3.x
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotNull;

Automated migration with OpenRewrite:

# build.gradle
plugins {
    id("org.openrewrite.rewrite") version "6.24.0"
}

rewrite {
    activeRecipe("org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_3")
}

# Execute migration
./gradlew rewriteRun

1.2 Java 21 Baseline

Spring Boot 3.x requires Java 17 minimum but is fully optimized for Java 21:

// Java 21 features you can use with Spring Boot 3.x

// 1. Record Patterns
public ResponseEntity<?> processPayment(Object payment) {
    return switch (payment) {
        case CreditCardPayment(var card, var amount)
            -> processCard(card, amount);
        case BankTransfer(var account, var amount)
            -> processTransfer(account, amount);
        default -> ResponseEntity.badRequest().build();
    };
}

// 2. Sealed Interfaces for domain modeling
public sealed interface PaymentResult
    permits PaymentSuccess, PaymentFailure, PaymentPending {
}
public record PaymentSuccess(String transactionId) implements PaymentResult {}
public record PaymentFailure(String reason) implements PaymentResult {}
public record PaymentPending(String retryAfter) implements PaymentResult {}
my-app/
  app-api/           # REST controllers, DTOs
  app-domain/        # Entities, repositories, services
  app-infra/         # External integrations, configs
  app-common/        # Shared utilities
  build.gradle       # Root build configuration

2. Java 21 Virtual Threads in Practice

2.1 Virtual Threads vs OS Threads

Virtual Threads are lightweight threads managed by the JVM, not the OS. They enable massive concurrency without the memory overhead of traditional platform threads.

// Platform Thread: 1:1 mapping to OS thread, ~1MB stack each
// 10,000 concurrent threads -> 10GB memory

// Virtual Thread: JVM-managed, ~1KB stack each
// 1,000,000 concurrent threads -> ~1GB memory

// Creating Virtual Threads
Thread.ofVirtual().start(() -> {
    System.out.println("Running on: " + Thread.currentThread());
});

// Executor for Virtual Threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 100_000).forEach(i ->
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        })
    );
}

2.2 Enabling Virtual Threads in Spring Boot

One line is all you need:

# application.yml
spring:
  threads:
    virtual:
      enabled: true

This replaces the Tomcat thread pool with Virtual Threads for all request handling.

2.3 Benchmark: Traditional vs Virtual Threads

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class VirtualThreadBenchmarkTest {

    @Autowired
    TestRestTemplate restTemplate;

    @Test
    void benchmark10kConcurrentRequests() throws Exception {
        int concurrency = 10_000;
        CountDownLatch latch = new CountDownLatch(concurrency);
        AtomicInteger success = new AtomicInteger(0);

        long start = System.nanoTime();

        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < concurrency; i++) {
                executor.submit(() -> {
                    try {
                        ResponseEntity<String> response =
                            restTemplate.getForEntity("/api/slow-endpoint", String.class);
                        if (response.getStatusCode().is2xxSuccessful()) {
                            success.incrementAndGet();
                        }
                    } finally {
                        latch.countDown();
                    }
                });
            }
            latch.await(60, TimeUnit.SECONDS);
        }

        long elapsed = Duration.ofNanos(System.nanoTime() - start).toMillis();
        System.out.printf("10K requests: %dms, success: %d%n", elapsed, success.get());
        // Virtual Threads: ~2,500ms with 100ms DB calls
        // Platform Threads (200 pool): ~50,000ms
    }
}

2.4 Virtual Threads vs WebFlux

// === Reactive approach (WebFlux) ===
@GetMapping("/users-reactive")
public Mono<List<User>> getUsersReactive() {
    return webClient.get()
        .uri("/api/users")
        .retrieve()
        .bodyToFlux(User.class)
        .collectList()
        .flatMap(users ->
            externalService.enrich(users)
                .map(enriched -> enriched)
        );
}

// === Virtual Thread approach (synchronous code, async performance) ===
@GetMapping("/users-virtual")
public List<User> getUsersVirtual() {
    List<User> users = restClient.get()
        .uri("/api/users")
        .retrieve()
        .body(new ParameterizedTypeReference<>() {});

    return externalService.enrich(users); // Blocking is fine!
}

Key insight: Virtual Threads let you write synchronous code while achieving throughput comparable to Reactive programming. Debugging is dramatically simpler.

2.5 Virtual Thread Pitfalls

// 1. NEVER use synchronized -> use ReentrantLock
// Bad: Virtual Thread gets pinned to carrier thread
public synchronized void updateCounter() {
    counter++;
}

// Good: ReentrantLock allows yielding
private final ReentrantLock lock = new ReentrantLock();
public void updateCounter() {
    lock.lock();
    try {
        counter++;
    } finally {
        lock.unlock();
    }
}

// 2. Minimize ThreadLocal usage
// Virtual Threads can number in millions
// ThreadLocal abuse causes memory explosion
// -> Use ScopedValue (Preview) instead

// 3. CPU-bound tasks still benefit from Platform Threads
@Bean
TaskExecutor cpuIntensiveExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
    executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 2);
    return executor;
}

// 4. Detect pinning with JVM flag
// -Djdk.tracePinnedThreads=short

3. GraalVM Native Image

3.1 Why Native Image?

GraalVM Native Image compiles Spring Boot apps to native binaries via AOT (Ahead-of-Time) compilation. The results are remarkable:

MetricJVM ModeNative Image
Startup time2-5s0.05-0.2s
Memory (RSS)200-500MB30-80MB
Peak throughputHigherModerate-High
Build timeFast3-10min
ReflectionUnrestrictedRequires hints

3.2 Native Image Build Configuration

// build.gradle
plugins {
    id 'org.graalvm.buildtools.native' version '0.10.3'
}

graalvmNative {
    binaries {
        main {
            imageName = 'my-app'
            mainClass = 'com.example.MyApplication'
            buildArgs.addAll(
                '--no-fallback',
                '-H:+ReportExceptionStackTraces',
                '--enable-url-protocols=http,https'
            )
        }
    }
}
# Build native image
./gradlew nativeCompile

# Run
./build/native/nativeCompile/my-app
# Started in 0.087 seconds!

3.3 Reflection Hints

@Configuration
@ImportRuntimeHints(AppRuntimeHints.class)
public class NativeImageConfig {
}

public class AppRuntimeHints implements RuntimeHintsRegistrar {

    @Override
    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
        // Register types for reflection
        hints.reflection()
            .registerType(UserDto.class, MemberCategory.values())
            .registerType(OrderDto.class, MemberCategory.values());

        // Register resources
        hints.resources()
            .registerPattern("templates/*")
            .registerPattern("static/**")
            .registerPattern("messages/*.properties");

        // Register JDK proxies
        hints.proxies()
            .registerJdkProxy(MyRepository.class);

        // Register serialization
        hints.serialization()
            .registerType(MyEvent.class);
    }
}

// Simpler alternative: annotation-based
@RegisterReflectionForBinding({UserDto.class, OrderDto.class})
@Configuration
public class ReflectionConfig {
}

3.4 Docker Multi-Stage Native Build

# Stage 1: Build native image
FROM ghcr.io/graalvm/native-image-community:21 AS builder
WORKDIR /app
COPY . .
RUN ./gradlew nativeCompile --no-daemon

# Stage 2: Minimal runtime
FROM debian:bookworm-slim
RUN addgroup --system spring && adduser --system --ingroup spring spring
USER spring:spring
COPY --from=builder /app/build/native/nativeCompile/my-app /app
EXPOSE 8080
ENTRYPOINT ["/app"]
docker build -t my-app:native .
docker run -p 8080:8080 my-app:native
# Started in 0.071 seconds, 42MB RSS

3.5 Native Image Limitations and Workarounds

// Issue: Dynamic class loading won't work
// Solution: Register at build time

// Issue: Reflection-heavy libraries (Jackson, Hibernate)
// Solution: Spring AOT auto-generates most hints

// Issue: Build time is long (5-10 minutes)
// Solution: Use JVM mode for dev, Native for production
// Use Cloud Native Buildpacks for CI/CD:
// ./gradlew bootBuildImage --imageName=my-app:native

4. Spring AI — LLM Integration

4.1 Spring AI Overview

Spring AI provides a consistent abstraction over multiple LLM providers (OpenAI, Anthropic, Ollama, etc.) with familiar Spring patterns.

dependencies {
    // Choose your provider
    implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
    // implementation 'org.springframework.ai:spring-ai-anthropic-spring-boot-starter'
    // implementation 'org.springframework.ai:spring-ai-ollama-spring-boot-starter'

    // Vector Store for RAG
    implementation 'org.springframework.ai:spring-ai-pgvector-store-spring-boot-starter'
}
# application.yml
spring:
  ai:
    openai:
      api-key: your-api-key-here
      chat:
        options:
          model: gpt-4o
          temperature: 0.7

4.2 ChatClient Usage

@RestController
@RequiredArgsConstructor
public class AiController {

    private final ChatClient.Builder chatClientBuilder;

    // Simple chat
    @GetMapping("/ai/chat")
    public String chat(@RequestParam String message) {
        return chatClientBuilder.build()
            .prompt()
            .user(message)
            .call()
            .content();
    }

    // Streaming response
    @GetMapping(value = "/ai/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> chatStream(@RequestParam String message) {
        return chatClientBuilder.build()
            .prompt()
            .user(message)
            .stream()
            .content();
    }

    // Structured output
    @GetMapping("/ai/analyze")
    public SentimentResult analyzeSentiment(@RequestParam String text) {
        return chatClientBuilder.build()
            .prompt()
            .user("Analyze the sentiment of this text: " + text)
            .call()
            .entity(SentimentResult.class);
    }
}

record SentimentResult(String sentiment, double confidence, String explanation) {}

4.3 RAG (Retrieval-Augmented Generation)

@Service
@RequiredArgsConstructor
public class RagService {

    private final ChatClient.Builder chatClientBuilder;
    private final VectorStore vectorStore;

    // Ingest documents into vector store
    public void ingestDocuments(List<Document> documents) {
        vectorStore.add(documents);
    }

    // Query with RAG context
    public String queryWithContext(String question) {
        // 1. Similarity search for relevant documents
        List<Document> relevantDocs = vectorStore.similaritySearch(
            SearchRequest.query(question).withTopK(5)
        );

        // 2. Build context from results
        String context = relevantDocs.stream()
            .map(Document::getContent)
            .collect(Collectors.joining("\n\n"));

        // 3. Query LLM with context
        return chatClientBuilder.build()
            .prompt()
            .system(s -> s.text("""
                You are a helpful assistant. Answer based on the provided context.
                If the answer is not in the context, say you don't know.

                Context:
                {context}
                """).param("context", context))
            .user(question)
            .call()
            .content();
    }
}

4.4 Function Calling (Tool Calling)

@Configuration
public class AiFunctionConfig {

    @Bean
    @Description("Get current weather for a given city")
    public Function<WeatherRequest, WeatherResponse> currentWeather() {
        return request -> {
            // Call actual weather API
            return new WeatherResponse(request.city(), 25.0, "Sunny");
        };
    }

    @Bean
    @Description("Search products in the catalog")
    public Function<ProductSearchRequest, List<Product>> searchProducts(
            ProductRepository productRepository) {
        return request -> productRepository
            .findByNameContaining(request.query());
    }
}

record WeatherRequest(String city) {}
record WeatherResponse(String city, double temperature, String condition) {}
record ProductSearchRequest(String query) {}

// Usage: LLM automatically calls registered functions
@GetMapping("/ai/assistant")
public String assistant(@RequestParam String question) {
    return chatClientBuilder.build()
        .prompt()
        .user(question) // "What's the weather in Seoul?"
        .functions("currentWeather", "searchProducts")
        .call()
        .content();
}

5. Spring Security 6

5.1 SecurityFilterChain Configuration

Spring Security 6 uses Lambda DSL exclusively and removes WebSecurityConfigurerAdapter.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            )
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers(HttpMethod.GET, "/api/posts/**").permitAll()
                .requestMatchers(HttpMethod.POST, "/api/posts/**").authenticated()
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtDecoder(jwtDecoder()))
            )
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint(
                    new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
            )
            .build();
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder
            .withJwkSetUri("https://auth.example.com/.well-known/jwks.json")
            .build();
    }
}

5.2 Method Security

@Configuration
@EnableMethodSecurity // replaces @EnableGlobalMethodSecurity
public class MethodSecurityConfig {}

@Service
public class PostService {

    @PreAuthorize("hasRole('ADMIN') or #authorId == authentication.principal.id")
    public void deletePost(Long postId, Long authorId) {
        // Only admin or the author can delete
    }

    @PostAuthorize("returnObject.author.id == authentication.principal.id")
    public Post getMyPost(Long postId) {
        return postRepository.findById(postId).orElseThrow();
    }
}

5.3 Security Headers for Production

@Bean
@Profile("prod")
public SecurityFilterChain productionSecurity(HttpSecurity http) throws Exception {
    return http
        .headers(headers -> headers
            .contentSecurityPolicy(csp -> csp
                .policyDirectives("default-src 'self'; script-src 'self'"))
            .frameOptions(frame -> frame.deny())
            .httpStrictTransportSecurity(hsts -> hsts
                .includeSubDomains(true)
                .maxAgeInSeconds(31536000))
            .permissionsPolicy(pp -> pp
                .policy("camera=(), microphone=(), geolocation=()"))
        )
        .build();
}

6. Data Access: JPA, R2DBC, QueryDSL

6.1 Spring Data JPA with Hibernate 6

@Entity
@Table(name = "posts")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 200)
    private String title;

    @Column(columnDefinition = "TEXT")
    private String content;

    @Enumerated(EnumType.STRING)
    private PostStatus status;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "author_id")
    private User author;

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Comment> comments = new ArrayList<>();

    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;

    // Business method
    public void publish() {
        if (this.status != PostStatus.DRAFT) {
            throw new IllegalStateException("Only draft posts can be published");
        }
        this.status = PostStatus.PUBLISHED;
    }
}

6.2 Solving the N+1 Problem

public interface PostRepository extends JpaRepository<Post, Long> {

    // Solution 1: EntityGraph
    @EntityGraph(attributePaths = {"author", "comments"})
    List<Post> findByStatus(PostStatus status);

    // Solution 2: Fetch Join
    @Query("SELECT DISTINCT p FROM Post p JOIN FETCH p.author WHERE p.status = :status")
    List<Post> findByStatusWithAuthor(@Param("status") PostStatus status);

    // Solution 3: Batch Size (global in application.yml)
    // spring.jpa.properties.hibernate.default_batch_fetch_size: 100
}

6.3 QueryDSL Integration

@Repository
@RequiredArgsConstructor
public class PostQueryRepository {

    private final JPAQueryFactory queryFactory;

    public Page<PostDto> searchPosts(PostSearchCondition condition, Pageable pageable) {
        QPost post = QPost.post;
        QUser user = QUser.user;

        List<PostDto> content = queryFactory
            .select(Projections.constructor(PostDto.class,
                post.id, post.title, post.status,
                user.name, post.createdAt))
            .from(post)
            .join(post.author, user)
            .where(
                titleContains(condition.title()),
                statusEquals(condition.status()),
                createdAfter(condition.fromDate())
            )
            .orderBy(post.createdAt.desc())
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .fetch();

        Long total = queryFactory
            .select(post.count())
            .from(post)
            .where(
                titleContains(condition.title()),
                statusEquals(condition.status()),
                createdAfter(condition.fromDate())
            )
            .fetchOne();

        return new PageImpl<>(content, pageable, total != null ? total : 0);
    }

    private BooleanExpression titleContains(String title) {
        return title != null ? QPost.post.title.contains(title) : null;
    }

    private BooleanExpression statusEquals(PostStatus status) {
        return status != null ? QPost.post.status.eq(status) : null;
    }

    private BooleanExpression createdAfter(LocalDateTime from) {
        return from != null ? QPost.post.createdAt.after(from) : null;
    }
}

6.4 R2DBC (Reactive Data Access)

public interface PostR2dbcRepository extends ReactiveCrudRepository<Post, Long> {

    Flux<Post> findByStatus(PostStatus status);

    @Query("SELECT * FROM posts WHERE title LIKE :keyword ORDER BY created_at DESC LIMIT :limit")
    Flux<Post> searchByTitle(String keyword, int limit);
}

7. Testcontainers Integration

7.1 ServiceConnection Auto-Configuration

@SpringBootTest
@Testcontainers
class PostServiceIntegrationTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres =
        new PostgreSQLContainer<>("postgres:16-alpine");

    @Container
    @ServiceConnection
    static GenericContainer<?> redis =
        new GenericContainer<>("redis:7-alpine")
            .withExposedPorts(6379);

    @Autowired
    private PostService postService;

    @Test
    void shouldCreateAndRetrievePost() {
        CreatePostRequest request = new CreatePostRequest("Title", "Content");
        Post created = postService.createPost(request);

        Post found = postService.getPost(created.getId());
        assertThat(found.getTitle()).isEqualTo("Title");
        assertThat(found.getStatus()).isEqualTo(PostStatus.DRAFT);
    }
}

7.2 Reusable Test Containers

@TestConfiguration(proxyBeanMethods = false)
public class TestcontainersConfig {

    @Bean
    @ServiceConnection
    PostgreSQLContainer<?> postgresContainer() {
        return new PostgreSQLContainer<>("postgres:16-alpine")
            .withReuse(true);
    }

    @Bean
    @ServiceConnection(name = "redis")
    GenericContainer<?> redisContainer() {
        return new GenericContainer<>("redis:7-alpine")
            .withExposedPorts(6379)
            .withReuse(true);
    }
}

// Run as development server with test containers
public class TestMyApplication {
    public static void main(String[] args) {
        SpringApplication.from(MyApplication::main)
            .with(TestcontainersConfig.class)
            .run(args);
    }
}

7.3 Kafka Testcontainer

@SpringBootTest
@Testcontainers
class KafkaIntegrationTest {

    @Container
    @ServiceConnection
    static KafkaContainer kafka = new KafkaContainer(
        DockerImageName.parse("confluentinc/cp-kafka:7.6.0")
    );

    @Autowired
    private KafkaTemplate<String, OrderEvent> kafkaTemplate;

    @Test
    void shouldProcessOrderEvent() throws Exception {
        OrderEvent event = new OrderEvent("order-123", "CREATED", BigDecimal.valueOf(99.99));

        kafkaTemplate.send("orders", event).get();

        await().atMost(Duration.ofSeconds(10))
            .untilAsserted(() ->
                assertThat(consumer.getProcessedOrders())
                    .hasSize(1)
                    .first()
                    .extracting(OrderEvent::orderId)
                    .isEqualTo("order-123")
            );
    }
}

8. Observability: Micrometer + OpenTelemetry

8.1 OTLP Configuration

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  metrics:
    distribution:
      percentiles-histogram:
        http.server.requests: true
    tags:
      application: my-spring-app
  tracing:
    sampling:
      probability: 1.0
  otlp:
    metrics:
      export:
        url: http://otel-collector:4318/v1/metrics
    tracing:
      endpoint: http://otel-collector:4318/v1/traces
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'io.micrometer:micrometer-tracing-bridge-otel'
    implementation 'io.opentelemetry:opentelemetry-exporter-otlp'
    runtimeOnly 'io.micrometer:micrometer-registry-prometheus'
}

8.2 Custom Metrics

@Service
public class OrderService {

    private final Counter orderCounter;
    private final Timer orderProcessingTimer;
    private final MeterRegistry meterRegistry;

    public OrderService(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.orderCounter = Counter.builder("orders.created")
            .description("Total orders created")
            .register(meterRegistry);
        this.orderProcessingTimer = Timer.builder("orders.processing.time")
            .description("Order processing duration")
            .register(meterRegistry);
    }

    public Order createOrder(CreateOrderRequest request) {
        return orderProcessingTimer.record(() -> {
            Order order = processOrder(request);
            orderCounter.increment();
            meterRegistry.gauge("orders.pending",
                orderRepository.countByStatus(OrderStatus.PENDING));
            return order;
        });
    }
}

8.3 Distributed Tracing

@RestController
@RequiredArgsConstructor
public class OrderController {

    private final OrderService orderService;
    private final ObservationRegistry observationRegistry;

    @PostMapping("/api/orders")
    public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
        return Observation.createNotStarted("order.creation", observationRegistry)
            .lowCardinalityKeyValue("order.type", request.type())
            .observe(() -> {
                Order order = orderService.createOrder(request);
                return ResponseEntity.created(
                    URI.create("/api/orders/" + order.getId())
                ).body(order);
            });
    }
}

8.4 Grafana Stack (Docker Compose)

# docker-compose-observability.yml
services:
  otel-collector:
    image: otel/opentelemetry-collector-contrib:latest
    ports:
      - "4317:4317"
      - "4318:4318"
    volumes:
      - ./otel-config.yaml:/etc/otelcol-contrib/config.yaml

  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin

  tempo:
    image: grafana/tempo:latest
    ports:
      - "3200:3200"
    command: [ "-config.file=/etc/tempo.yaml" ]

9. Docker and Kubernetes Deployment

9.1 Optimized Layered Dockerfile

# Use layered JAR for optimal Docker caching
FROM eclipse-temurin:21-jre-alpine AS builder
WORKDIR /app
COPY build/libs/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring

COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./

EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

9.2 Cloud Native Buildpacks

# No Dockerfile needed!
./gradlew bootBuildImage --imageName=myregistry/my-app:latest

# For native image
./gradlew bootBuildImage \
  --imageName=myregistry/my-app:native \
  --builder=paketobuildpacks/builder-jammy-tiny

9.3 Kubernetes Manifests

apiVersion: apps/v1
kind: Deployment
metadata:
  name: spring-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: spring-app
  template:
    metadata:
      labels:
        app: spring-app
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "8080"
        prometheus.io/path: "/actuator/prometheus"
    spec:
      containers:
        - name: spring-app
          image: myregistry/spring-app:latest
          ports:
            - containerPort: 8080
          env:
            - name: SPRING_PROFILES_ACTIVE
              value: "prod"
            - name: JAVA_OPTS
              value: "-XX:MaxRAMPercentage=75.0 -XX:+UseZGC"
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "1000m"
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10
          startupProbe:
            httpGet:
              path: /actuator/health
              port: 8080
            failureThreshold: 30
            periodSeconds: 2
---
apiVersion: v1
kind: Service
metadata:
  name: spring-app-service
spec:
  selector:
    app: spring-app
  ports:
    - port: 80
      targetPort: 8080
  type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: spring-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: spring-app
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

9.4 Graceful Shutdown

# application-prod.yml
server:
  shutdown: graceful

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s
@Component
@Slf4j
public class GracefulShutdownHandler {

    @PreDestroy
    public void onShutdown() {
        log.info("Shutting down gracefully...");
        // Finish in-flight requests
        // Close connection pools
        // Flush metrics
    }
}

10. Production Checklist

Security

  • CSRF protection for browser-based APIs
  • CORS restricted to allowed origins
  • Security headers (CSP, HSTS, X-Frame-Options)
  • Secrets externalized via environment variables or vault
  • Actuator endpoints secured and restricted

Performance

  • Virtual Threads enabled for I/O-heavy workloads
  • HikariCP pool size tuned (CPU cores x 2 + disk spindles)
  • JPA open-in-view set to false
  • Batch fetch size configured (100)
  • Connection timeouts configured (3s)

Reliability

  • Graceful shutdown enabled
  • Health probes configured (readiness + liveness)
  • Circuit breaker with Resilience4j
  • Retry with exponential backoff
  • DB migrations managed with Flyway

Operations

  • Structured JSON logging
  • Distributed tracing with OTLP
  • Prometheus metrics exposed
  • Alerting rules configured
  • Regular dependency updates with Renovate/Dependabot

Production application.yml

spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 3000
      idle-timeout: 600000
      max-lifetime: 1800000
  jpa:
    open-in-view: false
    hibernate:
      ddl-auto: none
    properties:
      hibernate:
        default_batch_fetch_size: 100
        jdbc.batch_size: 50
        order_inserts: true
        order_updates: true
  threads:
    virtual:
      enabled: true

server:
  shutdown: graceful
  tomcat:
    max-connections: 10000
    accept-count: 200
    connection-timeout: 5s

logging:
  level:
    root: WARN
    com.myapp: INFO
  pattern:
    console: "%d [%thread] %-5level %logger - traceId=%X{traceId} - %msg%n"

11. Interview Questions (15)

Fundamentals (1-5)

Q1. What are the major breaking changes in Spring Boot 3.x?

Jakarta EE migration (javax to jakarta), Java 17 minimum requirement, removal of WebSecurityConfigurerAdapter, Spring Security 6 Lambda DSL, and requestMatchers replacing antMatchers.

Q2. How do Virtual Threads differ from Reactive Programming?

Virtual Threads maintain a synchronous programming model while achieving high concurrency through JVM-managed lightweight threads. Reactive uses an asynchronous non-blocking model with operators. Virtual Threads excel at I/O-bound work; Reactive excels at streaming scenarios with backpressure.

Q3. What are the limitations of GraalVM Native Image?

Reflection, dynamic proxies, and JNI require explicit configuration via RuntimeHints. Build times are long (5-10 min). Peak throughput may be lower than JVM due to lack of JIT optimization. Some libraries may not be compatible.

Q4. Why was WebSecurityConfigurerAdapter removed?

To enable component-based security configuration using SecurityFilterChain beans, which is more flexible, testable, and allows multiple security filter chains for different URL patterns.

Q5. How does ServiceConnection work in Testcontainers?

It automatically detects the container type and maps its dynamic port and host information to Spring Boot auto-configuration properties, eliminating the need for manual DynamicPropertySource configuration.

Intermediate (6-10)

Q6. What is thread pinning in Virtual Threads?

When a Virtual Thread enters a synchronized block or performs a native call, it becomes pinned to its carrier platform thread, preventing other Virtual Threads from using that carrier. Use ReentrantLock instead of synchronized.

Q7. Explain the RAG pattern in Spring AI.

Documents are embedded into vectors and stored in a VectorStore. On query, similar documents are retrieved via similarity search, then passed as context to the LLM prompt along with the user question, grounding the response in factual data.

Q8. Three ways to solve the N+1 problem in JPA?

EntityGraph (declarative), Fetch Join (JPQL control), and Batch Size (global setting that groups lazy loading into IN clauses). Each has trade-offs regarding flexibility and query complexity.

Q9. What is the relationship between Micrometer and OpenTelemetry?

Micrometer is a metrics abstraction layer. OpenTelemetry is an observability standard (traces, metrics, logs). Spring Boot 3.x bridges them via Micrometer Tracing, exporting to OTLP-compatible backends.

Q10. Why set open-in-view to false?

When true (default), the persistence context stays open through the view layer, causing lazy-loaded queries during rendering and prolonged database connection usage. For API servers, this should always be false.

Advanced (11-15)

Q11. How does AOT processing work in Spring Boot?

At build time, Spring analyzes bean definitions, generates proxy classes, collects reflection metadata, and creates optimized bean factory code. This is a prerequisite for GraalVM Native Image and also speeds up JVM mode startup.

Q12. What is the difference between readiness and liveness probes?

Readiness probe (actuator/health/readiness) checks if the app can receive traffic. Failure removes it from the Service. Liveness probe (actuator/health/liveness) checks if the app is alive. Failure triggers a pod restart.

Q13. How to optimize HikariCP connection pool?

Set maximum-pool-size to CPU cores x 2 + disk spindles. Keep connection-timeout under 3 seconds. Set idle-timeout to 10 minutes and max-lifetime shorter than the database wait_timeout.

Q14. Explain Graceful Shutdown in Kubernetes context.

On SIGTERM, Spring stops accepting new requests while completing in-flight ones within the configured timeout. Combined with preStop hooks and readiness probe failure, this ensures zero-downtime rolling updates.

Q15. When to choose Virtual Threads over WebFlux?

Choose Virtual Threads for: existing synchronous codebases, simpler debugging needs, I/O-bound workloads. Choose WebFlux for: streaming use cases, backpressure requirements, existing reactive infrastructure, or when using non-blocking drivers exclusively.


12. Quiz

Q1. What is the one-line config to enable Virtual Threads in Spring Boot?

spring.threads.virtual.enabled=true in application.yml. This replaces the Tomcat thread pool with Virtual Threads for all request handling. Requires Spring Boot 3.2+ and Java 21+.

Q2. Why does reflection fail in GraalVM Native Image by default?

Native Image performs AOT compilation, which means class metadata is not available at runtime for dynamic reflection. You must register reflection targets via RuntimeHintsRegistrar or reflect-config.json at build time.

Q3. What replaced antMatchers() in Spring Security 6?

requestMatchers() replaced antMatchers(). This unified API works across both Servlet and Reactive environments, removing the tight coupling to the Servlet API that antMatchers had.

Q4. What does Testcontainers withReuse(true) do?

It reuses Docker containers between test runs instead of creating new ones each time, dramatically speeding up test execution. Requires testcontainers.reuse.enable=true in HOME/.testcontainers.properties.

Q5. How does Spring AI Function Calling work?

The LLM analyzes the user question and selects appropriate registered functions, extracting parameters. Spring AI executes the corresponding Java method and feeds the result back to the LLM, which generates the final response incorporating the function output.


References

  1. Spring Boot 3.3 Release Notes
  2. Spring Framework 6 Documentation
  3. JEP 444: Virtual Threads
  4. GraalVM Native Image Guide
  5. Spring AI Reference
  6. Spring Security 6 Migration Guide
  7. Testcontainers Spring Boot
  8. Micrometer Documentation
  9. OpenTelemetry Java SDK
  10. Spring Data JPA Reference
  11. HikariCP Configuration
  12. Spring Boot Docker Guide
  13. Kubernetes Spring Boot Best Practices
  14. OpenRewrite Spring Boot 3 Migration
  15. Baeldung Spring Boot 3 Tutorials
  16. Spring Boot Actuator Guide