Spring Boot 3.4: ๋ชจ๋“  ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ ์ƒ์„ธ ์„ค๋ช…

Spring Boot 3.4๋Š” ๋„ค์ดํ‹ฐ๋ธŒ ๊ตฌ์กฐํ™” ๋กœ๊น…, ํ™•์žฅ๋œ ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ, ๊ธฐ๋ณธ ๊ทธ๋ ˆ์ด์Šคํ’€ ์…ง๋‹ค์šด, MockMvcTester๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์˜ ์™„๋ฒฝ ๊ฐ€์ด๋“œ.

Spring Boot 3.4 ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ๊ณผ ๊ฐœ์„  ์‚ฌํ•ญ

Spring Boot 3.4๋Š” 2024๋…„ 11์›”์— ์ถœ์‹œ๋˜์–ด ๊ฐœ๋ฐœ์ž ์ƒ์‚ฐ์„ฑ๊ณผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ฑ๋Šฅ์—์„œ ์ƒ๋‹นํ•œ ๊ฐœ์„ ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ฒ„์ „์€ ๋„ค์ดํ‹ฐ๋ธŒ ๊ตฌ์กฐํ™” ๋กœ๊น…์„ ๋„์ž…ํ•˜๊ณ , ๊ทธ๋ ˆ์ด์Šคํ’€ ์…ง๋‹ค์šด์„ ๊ธฐ๋ณธ์œผ๋กœ ํ™œ์„ฑํ™”ํ•˜๋ฉฐ, ํ”„๋ ˆ์ž„์›Œํฌ ์ „๋ฐ˜์— ๊ฑธ์ณ ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ ์ง€์›์„ ํ™•์žฅํ•ฉ๋‹ˆ๋‹ค.

์ „์ œ ์กฐ๊ฑด

Spring Boot 3.4๋Š” ์ตœ์†Œ Java 17์ด ํ•„์š”ํ•˜๋ฉฐ, ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ๋ฅผ ์œ„ํ•ด Java 21์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ฒ„์ „์€ Spring Framework 6.2๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

๋„ค์ดํ‹ฐ๋ธŒ ๊ตฌ์กฐํ™” ๋กœ๊น…

๊ตฌ์กฐํ™” ๋กœ๊น…์€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ด€์ธก์„ฑ์—์„œ ์ค‘์š”ํ•œ ์ง„์ „์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. ํŒŒ์‹ฑํ•˜๊ธฐ ์–ด๋ ค์šด ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ๋กœ๊ทธ ๋Œ€์‹ , Spring Boot 3.4๋Š” Elasticsearch, Grafana Loki, Datadog ๊ฐ™์€ ๋„๊ตฌ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” JSON ํ˜•์‹์˜ ๋กœ๊ทธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

์„ธ ๊ฐ€์ง€ ํ˜•์‹์ด ๋„ค์ดํ‹ฐ๋ธŒ๋กœ ์ง€์›๋ฉ๋‹ˆ๋‹ค: Elastic Common Schema(ECS), Logstash, Graylog Extended Log Format(GELF).

properties
# application.properties
# Enable structured logging in console
logging.structured.format.console=ecs

# Or for log files
logging.structured.format.file=logstash

์ด ๊ฐ„๋‹จํ•œ ์„ค์ •์œผ๋กœ ์ž๋™์œผ๋กœ ํฌ๋งท๋œ ๊ตฌ์กฐํ™” JSON ๋กœ๊ทธ๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

LoggingController.javajava
@RestController
@RequestMapping("/api/demo")
public class LoggingController {

    // Logger injection via SLF4J
    private static final Logger logger = LoggerFactory.getLogger(LoggingController.class);

    @GetMapping("/action")
    public ResponseEntity<String> performAction(@RequestParam String userId) {
        // Log will be automatically formatted as structured JSON
        logger.info("Action performed by user: {}", userId);
        return ResponseEntity.ok("Action completed");
    }
}

ECS ํ˜•์‹์ด ํ™œ์„ฑํ™”๋˜๋ฉด ์ด ๋กœ๊ทธ๋Š” ํƒ€์ž„์Šคํƒฌํ”„, ๋ ˆ๋ฒจ, ๋ฉ”์‹œ์ง€, ํด๋ž˜์Šค๋ช…, ์Šค๋ ˆ๋“œ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•˜๋Š” JSON ๊ฐ์ฒด๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ์ด ๊ตฌ์กฐ๋Š” ๋ชจ๋‹ˆํ„ฐ๋ง ํ”Œ๋žซํผ์—์„œ์˜ ๊ฒ€์ƒ‰๊ณผ ์ง‘๊ณ„๋ฅผ ๋‹จ์ˆœํ™”ํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ณธ์œผ๋กœ ํ™œ์„ฑํ™”๋œ ๊ทธ๋ ˆ์ด์Šคํ’€ ์…ง๋‹ค์šด

์ค‘์š”ํ•œ ๋ณ€๊ฒฝ: ๊ทธ๋ ˆ์ด์Šคํ’€ ์…ง๋‹ค์šด์ด ์ด์ œ ๊ธฐ๋ณธ์œผ๋กœ ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค. ์ง„ํ–‰ ์ค‘์ธ HTTP ์š”์ฒญ์ด ์„œ๋ฒ„ ์ข…๋ฃŒ ์ „์— ์ฒ˜๋ฆฌ๋˜์–ด ๋ฐฐํฌ ์ค‘ 502 ์˜ค๋ฅ˜๋ฅผ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

properties
# application.properties
# Graceful shutdown is now ON by default
# To restore previous behavior (immediate shutdown):
server.shutdown=immediate

# Configure maximum wait timeout (30s default)
spring.lifecycle.timeout-per-shutdown-phase=45s

์ด ๋™์ž‘์€ ๋ชจ๋“  ์ž„๋ฒ ๋””๋“œ ์„œ๋ฒ„์— ์ ์šฉ๋ฉ๋‹ˆ๋‹ค: Tomcat, Jetty, Undertow, Reactor Netty.

LifecycleConfig.javajava
@Configuration
public class LifecycleConfig {

    private static final Logger logger = LoggerFactory.getLogger(LifecycleConfig.class);

    @Bean
    public ApplicationListener<ContextClosedEvent> gracefulShutdownListener() {
        // This listener executes at shutdown start
        return event -> {
            logger.info("Graceful shutdown initiated - completing in-flight requests");
            // Custom logic: close connections, save state, etc.
        };
    }
}

์ด ๊ฐœ์„ ์œผ๋กœ ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ ์ถ”๊ฐ€ ์„ค์ • ์—†์ด ๋ฌด์ค‘๋‹จ ๋ฐฐํฌ ๊ตฌํ˜„์ด ๊ฐ„์†Œํ™”๋ฉ๋‹ˆ๋‹ค.

ํ™•์žฅ๋œ ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ ์ง€์›

Spring Boot 3.4๋Š” ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ ์ง€์›(Java 21+)์„ ๋” ๋งŽ์€ ์ปดํฌ๋„ŒํŠธ๋กœ ํ™•์žฅํ•ฉ๋‹ˆ๋‹ค. OtlpMeterRegistry์™€ Undertow ์„œ๋ฒ„๊ฐ€ ํ™œ์„ฑํ™” ์‹œ ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

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

์ด ๋‹จ์ผ ์†์„ฑ์œผ๋กœ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์Šค๋ ˆ๋”ฉ ๋ชจ๋ธ์ด ๋ณ€ํ™˜๋ฉ๋‹ˆ๋‹ค. ๊ฐ HTTP ์š”์ฒญ์ด ์ž์ฒด ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ๋ฅผ ๋ฐ›์•„ ์Šค๋ ˆ๋“œ ํ’€์„ ๊ณ ๊ฐˆ์‹œํ‚ค์ง€ ์•Š๊ณ  ์ˆ˜์ฒœ ๊ฐœ์˜ ๋™์‹œ ์—ฐ๊ฒฐ์ด ๊ฐ€๋Šฅํ•ด์ง‘๋‹ˆ๋‹ค.

AsyncService.javajava
@Service
public class AsyncService {

    private static final Logger logger = LoggerFactory.getLogger(AsyncService.class);

    // With virtual threads enabled, each blocking call
    // no longer consumes an OS thread
    public String fetchExternalData() {
        logger.info("Executing on thread: {}", Thread.currentThread());

        // Blocking HTTP call - with virtual threads,
        // the OS thread is released during I/O wait
        return restClient.get()
            .uri("https://api.external.com/data")
            .retrieve()
            .body(String.class);
    }
}

๊ฐ€์ƒ ์Šค๋ ˆ๋“œ๋Š” I/O ์ง‘์•ฝ์ ์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๋น›์„ ๋ฐœํ•ฉ๋‹ˆ๋‹ค: HTTP ํ˜ธ์ถœ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ, ํŒŒ์ผ ์ž‘์—…. OS ์Šค๋ ˆ๋“œ๋Š” ๋Œ€๊ธฐ ์‹œ๊ฐ„ ๋™์•ˆ ํ•ด์ œ๋˜๊ณ  ์žฌ๊ฐœ ์‹œ ์žฌํ• ๋‹น๋ฉ๋‹ˆ๋‹ค.

ํ˜ธํ™˜์„ฑ

๊ฐ€์ƒ ์Šค๋ ˆ๋“œ๋Š” ์ตœ์†Œ Java 21์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. Java 17์—์„œ๋Š” ์ด ์†์„ฑ์ด ๋ฌด์‹œ๋˜๊ณ  ๊ธฐ์กด ๋™์ž‘์ด ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.

๊ฐœ์„ ๋œ RestClient์™€ RestTemplate

Spring Boot 3.4๋Š” HTTP ํด๋ผ์ด์–ธํŠธ ์„ค์ •์„ ํ‘œ์ค€ํ™”ํ•ฉ๋‹ˆ๋‹ค. HttpRequestFactory ์„ ํƒ์ด ์ด์ œ ํด๋ž˜์ŠคํŒจ์Šค ๊ธฐ๋ฐ˜์˜ ๋ช…ํ™•ํ•œ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค.

HttpClientConfig.javajava
@Configuration
public class HttpClientConfig {

    @Bean
    public RestClient restClient(RestClient.Builder builder) {
        // Spring Boot automatically chooses implementation:
        // 1. Apache HTTP Components (if present)
        // 2. Jetty Client
        // 3. Reactor Netty
        // 4. JDK HttpClient (Java 11+)
        // 5. SimpleClientHttpRequestFactory (fallback)
        return builder
            .baseUrl("https://api.example.com")
            .defaultHeader("Accept", "application/json")
            .build();
    }
}

๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ๋™์ž‘ ์„ค์ •๋„ ๊ฐ„์†Œํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

properties
# application.properties
# Force specific implementation
spring.http.client.factory=jdk

# Configure redirect behavior
spring.http.client.redirects=dont-follow

๋” ์„ธ๋ฐ€ํ•œ ์ œ์–ด๋ฅผ ์œ„ํ•ด ์ƒˆ๋กœ์šด ClientHttpRequestFactoryBuilder๊ฐ€ ์™„์ „ํ•œ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ฐฉ์‹ ์„ค์ •์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

CustomHttpClientConfig.javajava
@Configuration
public class CustomHttpClientConfig {

    @Bean
    public RestClient customRestClient(ClientHttpRequestFactoryBuilder factoryBuilder) {
        // Advanced factory configuration
        ClientHttpRequestFactory factory = factoryBuilder
            .httpComponents()
            .connectTimeout(Duration.ofSeconds(5))
            .readTimeout(Duration.ofSeconds(30))
            .build();

        return RestClient.builder()
            .requestFactory(factory)
            .baseUrl("https://api.example.com")
            .build();
    }
}

์ด ์ ‘๊ทผ ๋ฐฉ์‹์€ ๊ธฐ๋ณธ HTTP ๊ตฌํ˜„๊ณผ ๊ด€๊ณ„์—†์ด ์ผ๊ด€๋œ API๋กœ ์ตœ๋Œ€ํ•œ์˜ ์œ ์—ฐ์„ฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

Spring Boot ๋ฉด์ ‘ ์ค€๋น„๊ฐ€ ๋˜์…จ๋‚˜์š”?

์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ, flashcards, ๊ธฐ์ˆ  ํ…Œ์ŠคํŠธ๋กœ ์—ฐ์Šตํ•˜์„ธ์š”.

์œ ์ฐฝํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ MockMvcTester

Spring Boot 3.4๋Š” MockMvc์˜ AssertJ ๊ธฐ๋ฐ˜ ๋Œ€์•ˆ์ธ MockMvcTester๋ฅผ ๋„์ž…ํ•ฉ๋‹ˆ๋‹ค. ์ด ์ƒˆ๋กœ์šด ์ ‘๊ทผ ๋ฐฉ์‹์€ ํ…Œ์ŠคํŠธ๋ฅผ ๋” ์ฝ๊ธฐ ์‰ฝ๊ณ  ํ‘œํ˜„๋ ฅ ์žˆ๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

UserControllerTest.javajava
@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvcTester mockMvc; // New class!

    @Test
    void shouldReturnUserById() {
        // Fluent API with AssertJ
        mockMvc.get().uri("/api/users/{id}", 1)
            .assertThat()
            .hasStatusOk()
            .hasContentType(MediaType.APPLICATION_JSON)
            .bodyJson()
            .extractingPath("$.name")
            .isEqualTo("John Doe");
    }

    @Test
    void shouldReturn404ForUnknownUser() {
        mockMvc.get().uri("/api/users/{id}", 999)
            .assertThat()
            .hasStatus(HttpStatus.NOT_FOUND)
            .bodyJson()
            .extractingPath("$.error")
            .isEqualTo("User not found");
    }
}

๊ธฐ์กด MockMvc๋ฅผ ์‚ฌ์šฉํ•œ ์ ‘๊ทผ ๋ฐฉ์‹๊ณผ ๋น„๊ตํ•˜๋ฉด, ์–ด์„œ์…˜์ด ๋” ๊ฐ„๊ฒฐํ•˜๊ณ  ์ฒด์ด๋‹์ด ๋” ์ž์—ฐ์Šค๋Ÿฝ์Šต๋‹ˆ๋‹ค.

ComparisonTest.javajava
@WebMvcTest(UserController.class)
class ComparisonTest {

    @Autowired
    private MockMvcTester mockMvcTester;

    @Autowired
    private MockMvc mockMvc;

    @Test
    void withMockMvcTester() {
        // New approach: fluent and concise
        mockMvcTester.post().uri("/api/users")
            .contentType(MediaType.APPLICATION_JSON)
            .content("{\"name\": \"Jane\"}")
            .assertThat()
            .hasStatus(HttpStatus.CREATED)
            .hasHeader("Location", "/api/users/2");
    }

    @Test
    void withClassicMockMvc() throws Exception {
        // Old approach: more verbose
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"name\": \"Jane\"}"))
            .andExpect(status().isCreated())
            .andExpect(header().string("Location", "/api/users/2"));
    }
}

MockMvcTester๋Š” ํด๋ž˜์ŠคํŒจ์Šค์— AssertJ๊ฐ€ ์žˆ์„ ๋•Œ ์ž๋™ ์„ค์ •๋ฉ๋‹ˆ๋‹ค(spring-boot-starter-test์— ๊ธฐ๋ณธ ํฌํ•จ).

๊ฐœ์„ ๋œ Docker Compose์™€ Testcontainers

Docker Compose ์ง€์›์ด ์—ฌ๋Ÿฌ ์„ค์ • ํŒŒ์ผ๊ณผ ์ปค์Šคํ…€ ์ธ์ˆ˜๋กœ ์œ ์—ฐ์„ฑ์„ ํ™•๋ณดํ–ˆ์Šต๋‹ˆ๋‹ค.

properties
# application.properties
# Use multiple Docker Compose files
spring.docker.compose.file=compose.yaml,compose-dev.yaml

# Pass arguments at startup
spring.docker.compose.start.arguments=--scale redis=2

# Arguments at shutdown
spring.docker.compose.stop.arguments=--timeout 60

์ƒˆ๋กœ์šด ์„œ๋น„์Šค๊ฐ€ ์ž๋™์œผ๋กœ ๊ฐ์ง€๋˜๊ณ  ์„ค์ •๋ฉ๋‹ˆ๋‹ค.

yaml
# compose.yaml
services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    ports:
      - "5432:5432"

  redis-stack:
    image: redis/redis-stack:latest
    ports:
      - "6379:6379"
      - "8001:8001" # RedisInsight UI

  grafana-lgtm:
    image: grafana/otel-lgtm:latest
    ports:
      - "3000:3000"  # Grafana
      - "4317:4317"  # OTLP gRPC

Spring Boot 3.4๋Š” ์ด๋Ÿฌํ•œ ์„œ๋น„์Šค๋ฅผ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•˜๊ณ  ํ•ด๋‹น ์—ฐ๊ฒฐ ์†์„ฑ์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

Testcontainers ํ…Œ์ŠคํŠธ์—์„œ ์ƒˆ๋กœ์šด ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์ง€์›๋ฉ๋‹ˆ๋‹ค.

IntegrationTestConfig.javajava
@TestConfiguration(proxyBeanMethods = false)
public class IntegrationTestConfig {

    @Bean
    @ServiceConnection
    public PostgreSQLContainer<?> postgresContainer() {
        return new PostgreSQLContainer<>("postgres:16");
    }

    @Bean
    @ServiceConnection
    public RedisStackContainer redisStackContainer() {
        // New Redis Stack support
        return new RedisStackContainer("redis/redis-stack:latest");
    }

    @Bean
    @ServiceConnection
    public LgtmStackContainer observabilityContainer() {
        // New Grafana LGTM support (Loki, Grafana, Tempo, Mimir)
        return new LgtmStackContainer("grafana/otel-lgtm:latest");
    }
}

@ServiceConnection ์–ด๋…ธํ…Œ์ด์…˜์ด ์—ฐ๊ฒฐ ์†์„ฑ์„ ์ž๋™์œผ๋กœ ์„ค์ •ํ•˜์—ฌ ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ @DynamicPropertySource์˜ ํ•„์š”์„ฑ์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.

Actuator SSL๊ณผ ๊ด€์ธก์„ฑ

์ƒˆ๋กœ์šด /actuator/info ์—”๋“œํฌ์ธํŠธ๊ฐ€ ์„ค์ •๋œ SSL ์ธ์ฆ์„œ ์ •๋ณด๋ฅผ ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค: ์œ ํšจ ๊ธฐ๊ฐ„, ๋ฐœ๊ธ‰์ž, ์ฃผ์ฒด.

properties
# application.properties
# Enable SSL information in actuator
management.info.ssl.enabled=true

# Configure warning threshold for expiring certificates
management.health.ssl.certificate-validity-warning-threshold=30d

์ด ๊ธฐ๋Šฅ์œผ๋กœ actuator๋ฅผ ํ†ตํ•ด ์ธ์ฆ์„œ ๋งŒ๋ฃŒ๋ฅผ ์ง์ ‘ ๋ชจ๋‹ˆํ„ฐ๋งํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๊ฐฑ์‹  ์ž๋™ํ™”๊ฐ€ ์šฉ์ดํ•ด์ง‘๋‹ˆ๋‹ค.

SslMonitoringConfig.javajava
@Configuration
public class SslMonitoringConfig {

    @Bean
    public HealthIndicator sslCertificateHealth(SslInfo sslInfo) {
        return () -> {
            // Custom certificate validity check
            boolean allValid = sslInfo.getBundles().values().stream()
                .flatMap(bundle -> bundle.getCertificates().stream())
                .allMatch(cert -> cert.getValidityEnds().isAfter(Instant.now()));

            return allValid
                ? Health.up().build()
                : Health.down().withDetail("reason", "Certificate expiring soon").build();
        };
    }
}

๊ด€์ธก์„ฑ์„ ์œ„ํ•ด OTLP ์ „์†ก์ด ์ด์ œ HTTP ์™ธ์— gRPC๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

properties
# application.properties
# Use gRPC for OTLP (traces and metrics)
management.otlp.tracing.transport=grpc
management.otlp.tracing.endpoint=http://localhost:4317

# New: group applications together
spring.application.group=payment-services

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ทธ๋ฃน(spring.application.group)์œผ๋กœ ๊ด€์ธก์„ฑ ๋Œ€์‹œ๋ณด๋“œ์—์„œ ์—ฌ๋Ÿฌ ์„œ๋น„์Šค๋ฅผ ๋…ผ๋ฆฌ์ ์œผ๋กœ ๊ทธ๋ฃนํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋” ๊ฐ€๋ฒผ์šด OCI ์ด๋ฏธ์ง€

๊ธฐ๋ณธ OCI ์ด๋ฏธ์ง€ ๋นŒ๋”๊ฐ€ paketobuildpacks/builder-jammy-base์—์„œ paketobuildpacks/builder-jammy-java-tiny๋กœ ๋ณ€๊ฒฝ๋˜์–ด ํ›จ์”ฌ ์ž‘์€ ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

build.gradlegroovy
tasks.named("bootBuildImage") {
    // Native ARM support (new)
    imagePlatform = "linux/arm64"

    // New security flag
    trustBuilder = false

    // Image configuration
    imageName = "myregistry.com/myapp:${version}"
}

์ƒˆ๋กœ์šด imagePlatform ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ARM๊ณผ x64์˜ ํฌ๋กœ์Šค ํ”Œ๋žซํผ ๋นŒ๋“œ๊ฐ€ ๊ฐ„์†Œํ™”๋ฉ๋‹ˆ๋‹ค.

xml
<!-- pom.xml -->
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <image>
            <!-- Native ARM support -->
            <platform>linux/arm64</platform>
            <!-- Builder optimized for minimal images -->
            <builder>paketobuildpacks/builder-jammy-java-tiny</builder>
        </image>
    </configuration>
</plugin>

์ด๋Ÿฌํ•œ ์ตœ์ ํ™”๋œ ์ด๋ฏธ์ง€๋Š” ์‹œ์ž‘์ด ๋น ๋ฅด๊ณ  ๋ฆฌ์†Œ์Šค ์†Œ๋น„๊ฐ€ ์ ์–ด Kubernetes ๋ฐฐํฌ์— ํŠนํžˆ ์œ ๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ฐธ๊ณ 

๋นŒ๋” ๋ณ€๊ฒฝ์€ ํŠน์ • ์‹œ์Šคํ…œ ๋„๊ตฌ์— ์˜์กดํ•˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ์ „์— ์ƒˆ ์ด๋ฏธ์ง€๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

Bean Validation ๋ณ€๊ฒฝ ์‚ฌํ•ญ

Spring Boot 3.4๋Š” ๊ฒ€์ฆ ๋™์ž‘์„ Bean Validation ์‚ฌ์–‘์— ๋งž์ถฅ๋‹ˆ๋‹ค. ๊ฒ€์ฆ์ด ๋” ์ด์ƒ ์ค‘์ฒฉ๋œ ์†์„ฑ์— ์ž๋™์œผ๋กœ ์ „ํŒŒ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

ConfigProperties.javajava
@ConfigurationProperties(prefix = "app")
@Validated
public class AppConfig {

    @NotBlank
    private String name;

    // IMPORTANT: @Valid required to cascade validation
    @Valid
    private DatabaseConfig database;

    // Without @Valid, ServerConfig constraints will NOT be checked
    private ServerConfig server;

    // Getters and setters
}

public class DatabaseConfig {
    @NotBlank
    private String url;

    @Min(1)
    private int poolSize;

    // Getters and setters
}

public class ServerConfig {
    @NotNull // This constraint will NOT be checked without @Valid on parent
    private Integer port;

    // Getters and setters
}

์ด ๋ณ€๊ฒฝ์€ ๊ธฐ์กด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. @ConfigurationProperties ํด๋ž˜์Šค๋ฅผ ์ ๊ฒ€ํ•˜๊ณ  ๊ฒ€์ฆ์ด ์ „ํŒŒ๋˜์–ด์•ผ ํ•˜๋Š” ๊ณณ์— @Valid๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

@MockBean๊ณผ @SpyBean ์ง€์› ์ค‘๋‹จ

Spring Boot์˜ @MockBean๊ณผ @SpyBean ์–ด๋…ธํ…Œ์ด์…˜์ด ์ƒˆ๋กœ์šด Mockito ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ๋Œ€์ฒด๋˜์–ด ์ง€์› ์ค‘๋‹จ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

UserServiceTest.java (new approach)java
@SpringBootTest
class UserServiceTest {

    // New: native Mockito annotations
    @MockitoBean
    private UserRepository userRepository;

    @MockitoSpyBean
    private EmailService emailService;

    @Autowired
    private UserService userService;

    @Test
    void shouldCreateUser() {
        // Mock configuration
        when(userRepository.save(any())).thenReturn(new User(1L, "test@example.com"));

        userService.createUser("test@example.com");

        // Spy verification
        verify(emailService).sendWelcomeEmail("test@example.com");
    }
}

๊ธฐ์กด ์–ด๋…ธํ…Œ์ด์…˜์€ ์—ฌ์ „ํžˆ ๋™์ž‘ํ•˜์ง€๋งŒ ์ง€์› ์ค‘๋‹จ ๊ฒฝ๊ณ ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. @MockitoBean๊ณผ @MockitoSpyBean์œผ๋กœ์˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ๊ณ„ํšํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

์—ฐ์Šต์„ ์‹œ์ž‘ํ•˜์„ธ์š”!

๋ฉด์ ‘ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์™€ ๊ธฐ์ˆ  ํ…Œ์ŠคํŠธ๋กœ ์ง€์‹์„ ํ…Œ์ŠคํŠธํ•˜์„ธ์š”.

๊ฒฐ๋ก 

Spring Boot 3.4๋Š” ์ƒ์‚ฐ์„ฑ๊ณผ ๊ด€์ธก์„ฑ์—์„œ ์ƒ๋‹นํ•œ ๊ฐœ์„ ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค:

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ:

  • ๋” ๋‚˜์€ ๊ด€์ธก์„ฑ์„ ์œ„ํ•ด ๊ตฌ์กฐํ™” ๋กœ๊น… ํ™œ์„ฑํ™”
  • ๊ทธ๋ ˆ์ด์Šคํ’€ ์…ง๋‹ค์šด ๋™์ž‘ ํ™•์ธ (์ด์ œ ๊ธฐ๋ณธ ํ™œ์„ฑํ™”)
  • I/O ์ง‘์•ฝ์  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ ๊ณ ๋ ค (Java 21+)
  • ๋” ์ฝ๊ธฐ ์‰ฌ์šด ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด MockMvcTester๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
  • ์ค‘์ฒฉ๋œ @ConfigurationProperties ์†์„ฑ์— @Valid ์ถ”๊ฐ€
  • @MockBean/@SpyBean์„ @MockitoBean/@MockitoSpyBean์œผ๋กœ ๊ต์ฒด
  • ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ์ „ ์ƒˆ OCI ์ด๋ฏธ์ง€ ํ…Œ์ŠคํŠธ

Spring Boot 3.4๋Š” ๊ด€์ธก์„ฑ ํ‘œ์ค€๊ณผ ์„ฑ๋Šฅ์— ํŠน๋ณ„ํ•œ ๊ด€์‹ฌ์„ ๊ธฐ์šธ์ด๋ฉฐ ํ˜„๋Œ€ Java ๊ฐœ๋ฐœ์˜ ๋ ˆํผ๋Ÿฐ์Šค ํ”„๋ ˆ์ž„์›Œํฌ๋กœ์„œ์˜ ์ž…์ง€๋ฅผ ๊ฐ•ํ™”ํ•ฉ๋‹ˆ๋‹ค.

์ฐธ๊ณ  ์ž๋ฃŒ:

ํƒœ๊ทธ

#spring boot
#java
#spring boot 3.4
#spring framework
#backend

๊ณต์œ 

๊ด€๋ จ ๊ธฐ์‚ฌ