Spring Boot 3.4: ๋ชจ๋ ์๋ก์ด ๊ธฐ๋ฅ ์์ธ ์ค๋ช
Spring Boot 3.4๋ ๋ค์ดํฐ๋ธ ๊ตฌ์กฐํ ๋ก๊น , ํ์ฅ๋ ๊ฐ์ ์ค๋ ๋, ๊ธฐ๋ณธ ๊ทธ๋ ์ด์คํ ์ ง๋ค์ด, MockMvcTester๋ฅผ ์ ๊ณตํฉ๋๋ค. ์๋ก์ด ๊ธฐ๋ฅ์ ์๋ฒฝ ๊ฐ์ด๋.

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).
# application.properties
# Enable structured logging in console
logging.structured.format.console=ecs
# Or for log files
logging.structured.format.file=logstash์ด ๊ฐ๋จํ ์ค์ ์ผ๋ก ์๋์ผ๋ก ํฌ๋งท๋ ๊ตฌ์กฐํ JSON ๋ก๊ทธ๊ฐ ์์ฑ๋ฉ๋๋ค.
@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 ์ค๋ฅ๋ฅผ ๋ฐฉ์งํฉ๋๋ค.
# 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.
@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 ์๋ฒ๊ฐ ํ์ฑํ ์ ๊ฐ์ ์ค๋ ๋๋ฅผ ์ฌ์ฉํฉ๋๋ค.
# application.properties
# Enable virtual threads globally
spring.threads.virtual.enabled=true์ด ๋จ์ผ ์์ฑ์ผ๋ก ์ ํ๋ฆฌ์ผ์ด์ ์ ์ค๋ ๋ฉ ๋ชจ๋ธ์ด ๋ณํ๋ฉ๋๋ค. ๊ฐ HTTP ์์ฒญ์ด ์์ฒด ๊ฐ์ ์ค๋ ๋๋ฅผ ๋ฐ์ ์ค๋ ๋ ํ์ ๊ณ ๊ฐ์ํค์ง ์๊ณ ์์ฒ ๊ฐ์ ๋์ ์ฐ๊ฒฐ์ด ๊ฐ๋ฅํด์ง๋๋ค.
@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 ์ ํ์ด ์ด์ ํด๋์คํจ์ค ๊ธฐ๋ฐ์ ๋ช
ํํ ์ฐ์ ์์๋ฅผ ๋ฐ๋ฆ
๋๋ค.
@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();
}
}๋ฆฌ๋ค์ด๋ ํธ ๋์ ์ค์ ๋ ๊ฐ์ํ๋์์ต๋๋ค.
# application.properties
# Force specific implementation
spring.http.client.factory=jdk
# Configure redirect behavior
spring.http.client.redirects=dont-follow๋ ์ธ๋ฐํ ์ ์ด๋ฅผ ์ํด ์๋ก์ด ClientHttpRequestFactoryBuilder๊ฐ ์์ ํ ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์ ์ค์ ์ ๊ฐ๋ฅํ๊ฒ ํฉ๋๋ค.
@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๋ฅผ ๋์
ํฉ๋๋ค. ์ด ์๋ก์ด ์ ๊ทผ ๋ฐฉ์์ ํ
์คํธ๋ฅผ ๋ ์ฝ๊ธฐ ์ฝ๊ณ ํํ๋ ฅ ์๊ฒ ๋ง๋ญ๋๋ค.
@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๋ฅผ ์ฌ์ฉํ ์ ๊ทผ ๋ฐฉ์๊ณผ ๋น๊ตํ๋ฉด, ์ด์์
์ด ๋ ๊ฐ๊ฒฐํ๊ณ ์ฒด์ด๋์ด ๋ ์์ฐ์ค๋ฝ์ต๋๋ค.
@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 ์ง์์ด ์ฌ๋ฌ ์ค์ ํ์ผ๊ณผ ์ปค์คํ ์ธ์๋ก ์ ์ฐ์ฑ์ ํ๋ณดํ์ต๋๋ค.
# 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์๋ก์ด ์๋น์ค๊ฐ ์๋์ผ๋ก ๊ฐ์ง๋๊ณ ์ค์ ๋ฉ๋๋ค.
# 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 gRPCSpring Boot 3.4๋ ์ด๋ฌํ ์๋น์ค๋ฅผ ์๋์ผ๋ก ๊ฐ์งํ๊ณ ํด๋น ์ฐ๊ฒฐ ์์ฑ์ ์ค์ ํฉ๋๋ค.
Testcontainers ํ ์คํธ์์ ์๋ก์ด ์ปจํ ์ด๋๊ฐ ์ง์๋ฉ๋๋ค.
@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 ์ธ์ฆ์ ์ ๋ณด๋ฅผ ๋
ธ์ถํฉ๋๋ค: ์ ํจ ๊ธฐ๊ฐ, ๋ฐ๊ธ์, ์ฃผ์ฒด.
# 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๋ฅผ ํตํด ์ธ์ฆ์ ๋ง๋ฃ๋ฅผ ์ง์ ๋ชจ๋ํฐ๋งํ ์ ์์ผ๋ฉฐ, ๊ฐฑ์ ์๋ํ๊ฐ ์ฉ์ดํด์ง๋๋ค.
@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๋ฅผ ์ง์ํฉ๋๋ค.
# 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๋ก ๋ณ๊ฒฝ๋์ด ํจ์ฌ ์์ ์ด๋ฏธ์ง๋ฅผ ์์ฑํฉ๋๋ค.
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์ ํฌ๋ก์ค ํ๋ซํผ ๋น๋๊ฐ ๊ฐ์ํ๋ฉ๋๋ค.
<!-- 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 ์ฌ์์ ๋ง์ถฅ๋๋ค. ๊ฒ์ฆ์ด ๋ ์ด์ ์ค์ฒฉ๋ ์์ฑ์ ์๋์ผ๋ก ์ ํ๋์ง ์์ต๋๋ค.
@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 ์ด๋
ธํ
์ด์
์ผ๋ก ๋์ฒด๋์ด ์ง์ ์ค๋จ๋์์ต๋๋ค.
@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 ๋ฉด์ ์ง๋ฌธ 30์ : ์๋ฐ ๊ฐ๋ฐ์๋ฅผ ์ํ ์๋ฒฝ ๊ฐ์ด๋
์คํ ์ปจํผ๊ทธ๋ ์ด์ , ์คํํฐ, Spring Data JPA, ๋ณด์, ํ ์คํธ๋ฅผ ๋ง๋ผํ 30๋ฌธํญ์ผ๋ก Spring Boot ๋ฉด์ ์ ์ค๋นํ์ญ์์ค.

Spring Modulith: ๋ชจ๋๋ฌ ๋ชจ๋๋ฆฌ์ค ์ํคํ ์ฒ ํด์ค
Spring Modulith๋ก ์๋ฐ ๋ชจ๋๋ฌ ๋ชจ๋๋ฆฌ์ค๋ฅผ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋๋ค. ์ํคํ ์ฒ, ๋ชจ๋, ๋น๋๊ธฐ ์ด๋ฒคํธ, Spring Boot 3 ์์ ๋ก ์ดํด๋ณด๋ ํ ์คํธ.

Spring Batch 5 ๋ฉด์ : ํํฐ์ ๋, ์ฒญํฌ, ์ฅ์ ํ์ฉ
Spring Batch 5 ๋ฉด์ ์ ์ ๋ณตํ์ธ์. ํํฐ์ ๋, ์ฒญํฌ ์ฒ๋ฆฌ, ์ฅ์ ํ์ฉ์ ๊ดํ 15๊ฐ์ง ํต์ฌ ์ง๋ฌธ๊ณผ Java 21 ์์ ๋ฅผ ์ ๊ณตํฉ๋๋ค.