2025๋ 11์์ ์์ ๋ Spring Framework 7.0 ๋ฆด๋ฆฌ์ฆ ๋ ธํธ์ ๊ดํ ๊ฐ๋จํ ์ ๋ฆฌ ๋ฐ ๋ฒ์ญ ๊ธ.
Upgrading From Spring Framework 6.2
1. ์ฃผ์ ์์กด์ฑ ๋ฐ ์๊ตฌ์ฌํญ ๋ณ๊ฒฝ
์ต์ ์๊ตฌ์ฌํญ ์ํฅ
- JDK 17์ ๊ธฐ๋ณธ์ผ๋ก ์ ์งํ๋ฉด์ JDK 25 LTS ๊ถ์ฅ
- Jakarta EE 11 ๊ธฐ์ค์ ๋์
- Kotlin 2.2 ๋ฐ GraalVM 24 ์ง์
๊ตฌ์ฒด์ ์ธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ฒ์ ์ ๋ฐ์ดํธ:
- Servlet 6.1 (Tomcat 11.0, Jetty 12.1)
- JPA 3.2 (Hibernate ORM 7.1)
- Bean Validation 3.1 (Hibernate Validator 9.0)
- Netty 4.2, Kotlin 2.2, JSONassert 2.0
2. ์ ๊ฑฐ ๊ธฐ๋ฅ
๋ชจ๋ ์ ๊ฑฐ:
- spring-jcl ๋ชจ๋์ด Apache Commons Logging 1.3.0์ผ๋ก ๋์ฒด
์ด๋ ธํ ์ด์ ์ง์ ์ค๋จ:
- javax.annotation ๋ฐ javax.inject ํจํค์ง์ ์ด๋ ธํ ์ด์ ์ง์ ์ค๋จ
- @javax.annotation.Resource, @javax.annotation.PostConstruct, @javax.inject.Inject ๋ฑ์ jakarta.annotation ๋ฐ jakarta.inject ํจํค์ง๋ก ๋ง์ด๊ทธ๋ ์ด์ ํ์
Path Mapping ์ต์ ์ ๊ฑฐ:
- suffixPatternMatch/registeredSuffixPatternMatch, trailingSlashMatch, favorPathExtension/ignoreUnknownPathExtensions ๋ฑ ์์ ์ ๊ฑฐ
๊ธฐํ API ์ ๊ฑฐ:
- ListenableFuture → CompletableFuture๋ก ๋์ฒด
- WebJars ์ง์์์ webjars-locator-core → webjars-locator-lite๋ก ๋ณ๊ฒฝ
- OkHttp3 ์ง์ ์ ๊ฑฐ
3. ์ง์ ์ค๋จ(Deprecated) ๊ธฐ๋ฅ๋ค
- Spring MVC์ <mvc:*> XML ์ค์ ๋ค์์คํ์ด์ค๊ฐ Java ์ค์ ๋ฐฉ์์ผ๋ก ๊ถ์ฅ
- Kotlin ์คํฌ๋ฆฝํธ ํ ํ๋ฆฟ ์ง์ ์ค๋จ
- Spring TestContext Framework์ JUnit 4 ์ง์ ์ค๋จ, JUnit Jupiter์ SpringExtension ๊ถ์ฅ
- Jackson 2.x ์ง์ ์ค๋จ, Jackson 3.x๋ก ์ ํ
- Spring MVC์์ PathMatcher ์ฌ์ฉ ์ค๋จ
- HandlerMappingIntrospector SPI ์ค๋จ
4. ์ฃผ์ API ๋ณ๊ฒฝ์ฌํญ
Null Safety:
- JSR 305 ๊ธฐ๋ฐ์ Spring nullness ์ด๋ ธํ ์ด์ ์ด JSpecify ์ด๋ ธํ ์ด์ ์ผ๋ก ๋ณ๊ฒฝ
HttpHeaders API:
- HttpHeaders ํด๋์ค๊ฐ ๋ ์ด์ MultiValueMap ๊ณ์ฝ์ ํ์ฅํ์ง ์์
- ์ฌ๋ฌ ๋ฉ์๋๊ฐ ์ ๊ฑฐ๋๊ณ HttpHeaders#asMultiValueMap ๊ฐ์ ๋์ฒด ๋ฉ์๋๊ฐ @Deprecated๋ก ํ์
GraalVM ๋ฉํ๋ฐ์ดํฐ ํ์ ๋ณ๊ฒฝ:
- ๋ฆฌ์์ค ํํธ ๊ตฌ๋ฌธ์ด java.util.regex.Pattern์์ "glob pattern" ํ์์ผ๋ก ๋ณ๊ฒฝ
- ํ์ ์ ๋ํ reflection ํํธ ๋ฑ๋ก์ด ๋ฉ์๋, ์์ฑ์, ํ๋ ๊ฒ์ฌ๋ฅผ ์์ํ๋๋ก ๋ณ๊ฒฝ
New and Noteworthy
1. ์๋ก์ด ํ๋ก๊ทธ๋๋ฐ ๋ชจ๋ธ
BeanRegistrar ๊ณ์ฝ:
- @Configuration ํด๋์ค์ ๋จ์ผ @Bean ๋ฉ์๋ ๋ด์์ ์ฌ๋ฌ ๋น์ ๋ฑ๋ก์ ํด์๋ ์๋จ. (@Bean ๋ฉ์๋ ํ๋๋น ํ๋์ ๋น๋ง ๋ฑ๋ก)
- @Bean ๋ฉ์๋๋ ๋ฐํ ํ์ ์ ์ ์ธํ ๋ ๊ฐ๋ฅํ ๊ตฌ์ฒด์ ์ธ ํ์ ์ ๋ช ์๋ฅผ ๊ถ์ฅ (๋ฐํ ํ์ ์ List๋ Map ๊ฐ์ ์ถ์ ํ์ ์ด ์๋๋ผ ์ค์ ๊ตฌ์ฒด ํด๋์ค ํ์ ์ ์ฐ๋ ๊ฒ ์์น)
2. SpEL ํํ์ ๊ฐ์
Optional ํ์ ์ง์ ๊ฐํ:
- java.util.Optional ํ์ ์ ๋ํ null-safe ์ฐ์ฐ ์ง์ (null-safe ์ฐ์ฐ์(?.) ๋ฅผ ์ฌ์ฉ)
- Elvis ์ฐ์ฐ์๋ฅผ ์ฌ์ฉํ Optional ์๋ ์ธ๋ํ ์ง์
user?.name
- ๋์ ๋ฐฉ์
- user๊ฐ null ๋๋ Optional.empty() → ๊ฒฐ๊ณผ๋ null
- ๊ฐ์ด ์์ผ๋ฉด user.get().getName() ํธ์ถ๊ณผ ๋์ผ
name?.orElse('Unknown')
- Optional ์์ฒด์ ๋ฉ์๋(orElse, map, filter ๋ฑ)๋ ๊ทธ๋๋ก ํธ์ถ ๊ฐ๋ฅ
- name์ด ๋น์ด ์์ผ๋ฉด "Unknown" ๋ฐํ
- ๊ฐ์ด ์์ผ๋ฉด name.get() ๋ฐํ
names?.?[#this.length > 5]
- ์ปฌ๋ ์
Optional ์์
- names๊ฐ null ๋๋ Optional.empty() → ๊ฒฐ๊ณผ๋ null
- ๊ฐ์ด ์์ผ๋ฉด ๋ด๋ถ List<String>์ ๋์์ผ๋ก ๊ธธ์ด๊ฐ 5 ์ด๊ณผ์ธ ๋ฌธ์์ด ํํฐ๋ง ์ํ
(names.get().stream().filter(...).toList()์ ๋์ผ)
3. ํ๋ก์ ์ค์ ๊ฐ์
๊ธ๋ก๋ฒ ํ๋ก์ ํ์ ๊ธฐ๋ณธ๊ฐ:
- Spring Boot์ ๊ฐ์ CGLIB ๊ธ๋ก๋ฒ ํ๋ก์ ํ์ ๊ธฐ๋ณธ๊ฐ์ด ๋ชจ๋ ํ๋ก์ ํ๋ก์ธ์(@Async ๋ฑ ํฌํจ)์ ์ผ๊ด๋๊ฒ ์ ์ฉ
- ์ปค์คํ ๋ชฉ์ ์ ์ํด AutoProxyUtils.DEFAULT_PROXY_CONFIG_BEAN_NAME ์ด๋ฆ์ผ๋ก ProxyConfig ํ์ ์ ๋น ์ ์ธ ๊ฐ๋ฅ
@Proxyable ์ด๋ ธํ ์ด์ :
- ๊ฐ๋ณ ๋น์ ๋ํ ํ๋ก์ ์ค์ ์ ์ด๋ฅผ ์ํ ์๋ก์ด @Proxyable ์ด๋ ธํ ์ด์
- @Proxyable(INTERFACES) ๋๋ @Proxyable(TARGET_CLASS)๋ก ํ๋ก์ ํ์ ์ง์ ๊ฐ๋ฅ
- @Proxyable(interfaces=MyService.class)๋ฅผ ํตํ ๋น๋ณ ํ๋ก์ ์ธํฐํ์ด์ค ์ง์ ๊ฐ๋ฅ
4. Spring Retry ํตํฉ
Core ๋ชจ๋ ํตํฉ:
- Spring Retry ํ๋ก์ ํธ๊ฐ spring-core ๋ชจ๋์ org.springframework.core.retry ํจํค์ง๋ก ํตํฉ
- ๋ถํ์ํ ๊ธฐ๋ฅ ์ ๊ฑฐ ๋ฐ API ์ฌ๊ฒํ ๋ฅผ ํตํ ๊ธฐ์ด์ ์ธ ์ฌ์๋ ์ง์ ์ ๊ณต
Resilience ์ด๋ ธํ ์ด์ :
- spring-context ๋ชจ๋์ @Retryable ์ด๋ ธํ ์ด์ ์ง์
- Spring์ ๋์์ฑ ์ค๋กํ ์ง์ ๊ธฐ๋ฐ @ConcurrencyLimit ์ด๋ ธํ ์ด์
- @EnableResilientMethods๋ฅผ ํตํ ํธ๋ฆฌํ ํ์ฑํ
- @Retryable์ด ๋ฆฌ์กํฐ๋ธ ๋ฉ์๋์ ์๋ ์ ์ํ์ฌ Reactor์ ์ฌ์๋ ๊ธฐ๋ฅ์ผ๋ก ํ์ดํ๋ผ์ธ ์ฅ์
5. ํ ์คํธ ์ปจํ ์คํธ ๊ด๋ฆฌ ๊ฐ์
์๋ ์ปจํ ์คํธ ๊ด๋ฆฌ:
- ํ ์คํธ ์คํ ์ ์์ฑ๋ ApplicationContext๋ ํ ์คํธ ์ปจํ ์คํธ ์บ์์ ์ ์ฅ๋จ.
- Spring 7.0๋ถํฐ๋ ์บ์์ ์ ์ฅ๋ ์ปจํ ์คํธ๊ฐ ๋ ์ด์ ์ฌ์ฉ๋์ง ์์ผ๋ฉด ์๋์ผ๋ก pause ์ํ๋ก ์ ํ๋จ.
- ๋ค์ ํ์ํ ๋ ์บ์์์ ๊ฐ์ ธ์ค๋ฉด ์๋์ผ๋ก restart ๋์ด ์๋์ ๋ผ์ดํ์ฌ์ดํด ์ํ๊ฐ ๋ณต์๋จ
์ปจํ ์คํธ ์บ์ ๋ชจ๋ํฐ๋ง:
logging.level.org.springframework.test.context.cache=DEBUG
- ํ ์คํธ๊ฐ ๋ง์ ๊ฒฝ์ฐ ์ฌ๋ฌ ApplicationContext๊ฐ ์บ์์ ์์ฌ ์คํ ์๋๊ฐ ๋๋ ค์ง ์ ์์.
- ์บ์ ํํฉ(์ปจํ ์คํธ ๋ช ๊ฐ ๋ก๋๋์๋์ง, ์ผ๋ง๋ ์บ์๋์ด ์๋์ง)์ ๋ณด๋ ค๋ฉด:๋ก๊น ๋ ๋ฒจ์ ์ค์ ํ๋ฉด ์บ์ ์ํ๋ฅผ ๋ก๊ทธ์์ ํ์ธ ๊ฐ๋ฅ.
์๋ ์ผ์ ์ ์ง ํจ๊ณผ:
- ApplicationContext ๋ด๋ถ์ ์๋ ์์(auto-startup) Bean ๋ค๋ ํจ๊ป ์ ์ง๋จ.
- (์: JMS ๋ฆฌ์ค๋ ์ปจํ ์ด๋ + ์ค์ผ์ค๋ฌ ํ์คํฌ + Lifecycle / SmartLifecycle ์ธํฐํ์ด์ค ๊ตฌํ์ฒด
- ํ ์คํธ ์ค์ ์ฌ์ฉ๋์ง ์๋ ๋์์๋ ๋ฐฑ๊ทธ๋ผ์ด๋ ํ๋ก์ธ์ค๊ฐ ๋ถํ์ํ๊ฒ ์คํ๋์ง ์์.
- ๋จ, SmartLifecycle#isPauseable() = false๋ฅผ ๋ฐํํ๋ฉด ํด๋น ์ปดํฌ๋ํธ๋ ์ผ์ ์ ์ง์์ ์ ์ธ ๊ฐ๋ฅ.
6. ์๋ก์ด ํด๋ผ์ด์ธํธ ์ง์
JmsClient ๋์ :
- JdbcClient์ RestClient์ ์ด์ด ์๋ก์ด JmsClient ๋์
- JMS ๋์์ ๋ํ ์ผ๋ฐ์ ์ธ send/receive ์ฐ์ฐ ์ ๊ณต
- Spring์ ๊ณตํต Message ๋๋ ํ์ด๋ก๋ ๊ฐ ์ฒ๋ฆฌ, spring-messaging ๋ชจ๋๊ณผ ์ผ์นํ๋ MessagingException ๋ฐ์
- JmsMessagingTemplate์ ๋์์ผ๋ก Spring JmsTemplate์ ์์ํ์ฌ ์ค์ ์ฐ์ฐ ์ํ
JdbcClient ๊ฐ์ :
- fetch size, max rows, query timeout ๋ฑ ๋ช ๋ น๋ฌธ ์์ค ์ค์ ์ ํธ๋ฆฌํ๊ฒ ์ ๊ณต
7. API ๋ฒ์ ๊ด๋ฆฌ ์ง์
Spring MVC & WebFlux๊ฐ API ๋ฒ์ ๊ด๋ฆฌ์ ๋ํ ์ผ๊ธ(first-class) ์ง์์ ์ ๊ณต:
- ์๋ฒ: ์์ฒญ ๋ฒ์ ์ ๋ฐ๋ผ ์ปจํธ๋กค๋ฌ/๋ผ์ฐํ ์ ์ด ๊ฐ๋ฅ
- ํด๋ผ์ด์ธํธ: ์์ฒญ์ API ๋ฒ์ ์ง์ ๊ฐ๋ฅ
- ํ ์คํธ: MockMvc, WebTestClient ์์ ๋ฒ์ ๊ธฐ๋ฐ ์์ฒญ ํ ์คํธ ๊ฐ๋ฅ
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.ApiVersion;
@RestController
class UserController {
// v1 API
@GetMapping("/users")
@ApiVersion("1")
public String getUsersV1() {
return "User list v1";
}
// v2 API
@GetMapping("/users")
@ApiVersion("2")
public String getUsersV2() {
return "User list v2 with more details";
}
}
import org.springframework.web.reactive.function.client.WebClient;
public class ApiClient {
private final WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:8080")
.defaultHeader("API-Version", "2") // API ๋ฒ์ ์ง์
.build();
public String getUsers() {
return webClient.get()
.uri("/users")
.retrieve()
.bodyToMono(String.class)
.block();
}
}
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void testV1Users() throws Exception {
mockMvc.perform(get("/users").header("API-Version", "1"))
.andExpect(status().isOk())
.andExpect(content().string("User list v1"));
}
@Test
void testV2Users() throws Exception {
mockMvc.perform(get("/users").header("API-Version", "2"))
.andExpect(status().isOk())
.andExpect(content().string("User list v2 with more details"));
}
}
8. ์คํธ๋ฆฌ๋ฐ ๋ฐ ์ ์ถ๋ ฅ ๊ฐ์
- StreamingHttpOutputMessage.Body ๋ฉ์๋ ๋งค๊ฐ๋ณ์๋ฅผ ํตํ ์์ฒญ ๋ณธ๋ฌธ์ ๋ํ OutputStream ์ ๊ณต ๊ฐ๋ฅ
- InputStream ๋๋ ResponseEntity<InputStream> ๋ฐํ ๊ฐ์ ํตํ ์๋ต ์๋น ๊ฐ๋ฅ
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.io.*;
@RestController
@RequestMapping("/files")
public class FileController {
// ํ์ผ ์
๋ก๋ (์คํธ๋ฆฌ๋ฐ ๋ฐฉ์์ผ๋ก OutputStream ์ฒ๋ฆฌ)
@PostMapping(value = "/upload", consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<String> uploadFile(InputStream inputStream) throws IOException {
File targetFile = new File("uploaded.dat");
try (OutputStream outputStream = new FileOutputStream(targetFile)) {
inputStream.transferTo(outputStream); // InputStream → File
}
return ResponseEntity.ok("File uploaded successfully!");
}
// ํ์ผ ๋ค์ด๋ก๋ (ResponseEntity<InputStream> ํ์ฉ)
@GetMapping(value = "/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<InputStream> downloadFile() throws FileNotFoundException {
File file = new File("uploaded.dat");
InputStream inputStream = new FileInputStream(file);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(inputStream);
}
}
9. Path Matching ๊ฐ์
PathPattern ์์ ์ ํ:
- HTTP ์์ฒญ ๋งคํ์ ์ํ ๋ ๊ฑฐ์ AntPathMatcher ๋ณํ ์ง์ ์ค๋จ (spring reference - URI patterns)
- ๊ฒฝ๋ก ์์ ๋ถ๋ถ์์ ๋ง์ ๊ฒฝ๋ก ์ธ๊ทธ๋จผํธ ๋งค์นญ ๊ธฐ๋ฅ ์ถ๊ฐ (์: "/**/pages/index.html")
๐ PathPattern vs AntPathMatcher

10 .HTTP ๋ฉ์์ง ์ปจ๋ฒํฐ ๊ฐ์
1. ๊ธฐ์กด ๋ฐฉ์
- Spring MVC์์ HTTP ์์ฒญ/์๋ต ๋ณํ์ HttpMessageConverter ๋ค์ด ๋ด๋น.
- ํ์ง๋ง ์ค์ ์ด ํฉ์ด์ ธ ์๊ฑฐ๋, ๊ฐ๋ณ Bean ๋ฑ๋ก ๋ฐฉ์์ด ํ์ํด์ ์ ์ญ์ ์ผ๋ก ์ผ๊ด๋๊ฒ ๊ด๋ฆฌํ๊ธฐ ์ด๋ ค์.
- WebFlux(Reactor ๊ธฐ๋ฐ)์๋ CodecConfigurer ๊ฐ์ ์ค์ํ๋ ์ค์ API๊ฐ ์์์ง๋ง, Spring MVC/RestTemplate ์ชฝ์ ์๋์ ์ผ๋ก ๋ถํธํ์.
2. ์๋ก์ด HttpMessageConverters ํด๋์ค ๋์
- Spring 7.0์์ HttpMessageConverters ๋ผ๋ ์ค์ ์ง์ค์ ๊ด๋ฆฌ ํด๋์ค ๋์ .
- ํน์ง:
- ํด๋์คํจ์ค ์๋ ๊ฐ์ง (์: Jackson, Gson, JAXB ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ)
- ์ ์ญ(Global) ๊ตฌ์ฑ ๊ฐ๋ฅ
- ์ผ๊ด๋ API ์ ๊ณต → MVC, RestTemplate, RestClient ๋ชจ๋ ์ง์
3. ์ฅ์
- ์ค์ํ๋ ๋ฉ์์ง ๋ณํ๊ธฐ ์ค์ API ์ ๊ณต
- Jackson, Gson ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์๋ ๊ฐ์ง
- ์๋ฒ(MVC)์ ํด๋ผ์ด์ธํธ(RestClient, RestTemplate)์์ ์ผ๊ด๋ ๊ตฌ์ฑ ๊ฒฝํ ์ ๊ณต
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureMessageConverters(HttpMessageConverters.ServerBuilder builder) {
JsonMapper jsonMapper = JsonMapper.builder()
.findAndAddModules()
.enable(SerializationFeature.INDENT_OUTPUT) // pretty print
.defaultDateFormat(new SimpleDateFormat("yyyy-MM-dd")) // ๋ ์ง ํฌ๋งท ์ง์
.build();
builder.jsonMessageConverter(new JacksonJsonHttpMessageConverter(jsonMapper));
}
}
๐ ServerBuilder ๋ฅผ ํตํด ์ํ๋ ์ปค์คํ JSON Converter๋ฅผ ์ถ๊ฐ/๋์ฒด ๊ฐ๋ฅ.
11. ํ ์คํธ ์ง์ ๊ฐ์
RestTestClient:
- WebTestClient์ ๋น๋ฆฌ์กํฐ๋ธ ๋ณํ์ธ ์๋ก์ด RestTestClient ์ ๊ณต
- ๋ผ์ด๋ธ ์๋ฒ, MVC @Controller ๋๋ ์ ํ๋ฆฌ์ผ์ด์ ์ปจํ ์คํธ์ ๋ฐ์ธ๋ฉ ๊ฐ๋ฅ
- WebTestClient์ ๊ฐ์ fluent API ๋ฐ ์ด์ค์ ๊ธฐ๋ฅ ์ ๊ณต
