해당 글에서는 Spring Boot 4가 출시되어 주요 내용에 대한 Release Note를 읽어보기 위해 작성한 글입니다.
1) Spring Boot 4
💡 Spring Boot 4
- 2025년 11월 공식 릴리스된 Spring Boot의 메이저 버전 업그레이드로, 이전의 3.x 라인과 비교해 상당한 아키텍처 개선과 신기능이 도입된 새로운 세대의 프레임워크입니다.
- Spring Boot는 Java 생태계에서 가장 널리 쓰이는 백엔드 프레임워크로, 설정을 최소화하면서 Spring 기반 애플리케이션을 빠르게 만들도록 도와줍니다. - 버전 4.0은 Spring Framework 7을 기반으로 하며, 클라우드·컨테이너·모던 Java 생태계에 더 적합하도록 여러 부분이 근본적으로 재설계되었습니다 - Spring Boot 4 버전에서는 Java 25에 대한 완벽한 지원을 제공하며, Java 17과의 호환성도 유지합니다.
- Spring Boot4 애플리케이션 빌드에 Gradle 9가 지원됩니다. - 기존 Gradle 8.x(8.14 이상)에 대한 지원은 계속 유지됩니다.
HTTP Service Interface Clients
- HTTP 서비스를 호출할 때 RestClient나 WebClient를 직접 사용하는 대신, @HttpExchange 계열 어노테이션(@GetExchange, @PostExchange, @DeleteExchange 등)이 붙은 Java 인터페이스를 통해 선언적으로 호출할 수 있습니다.
API Versioning
- Spring Boot 4 (Spring Framework 7.x 이후)에서는 요청 Header를 조건으로 사용하는 API 버저닝 방식을 보다 자연스럽게 적용할 수 있습니다.
JmsClient
- JMS(Java Message Service) 내에서 기존 JmsTemplate, JmsMessagingTemplate를 사용했던 부분에 대해서 Spring에서 RestClient / HttpService Clients 스타일에 맞춘 JmsClient API가 추가되었습니다.
OpenTelemetry starter
- OpenTelemetry(OTel) 는 관측성(Observability)을 위한 표준 스펙 + 라이브러리 집합이며, 내부 동작에 대해서 관측하는 라이브러리가 Spring Boot Starter로 추가되었습니다.
[더 알아보기] 💡 왜 LTS(Long Term Support) 버전을 이용해야 할까? 1. 운영 환경에서의 안정성·보안·유지비용을 최소화하기 위해서입니다. - LTS 버전의 경우는 3 ~ 5년 이상의 보안 패치를 제공하지만, 일반 버전의 경우는 6개월 ~ 1년 내 종료가 됩니다.
2. 프레임워크, 라이브러리는 LTS를 기준으로 발전을 합니다 - Spring Boot, Hibernate, Gradle / Maven의 경우는 새로운 버전 LTS JVM을 기준으로 지원을 합니다.
3. 팀 조직 간의 관점에서 관리가 쉽습니다. - 팀 내에서 각각 다른 버전을 이용하는 근거가 부족하기에 조직 내에서 동일한 버전 환경에서 개발을 할 때에 유용합니다.
3) HTTP Service Interface Clients
💡 HTTP Service Interface Clients
- HTTP 서비스를 호출할 때 RestClient나 WebClient를 직접 사용하는 대신, @HttpExchange 계열 어노테이션 (@GetExchange, @PostExchange, @DeleteExchange 등)이 붙은 Java 인터페이스를 통해 선언적으로 호출할 수 있습니다.
- HTTP Service Interface Clients는 Spring Boot 4에서 자동 구성 및 구성 속성 지원이 추가되었으며, 일반 Java 인터페이스에 어노테이션을 선언하면 Spring이 런타임에 프락시 기반 구현체를 자동으로 생성합니다. - 기존에는 RestClient나 WebClient를 직접 사용하여 외부 HTTP 통신을 수행했다면, Spring Boot 4에서는 개발자가 HTTP Service Interface Clients의 인터페이스 메서드를 호출하면 Spring Proxy가 이를 가로어 내부적으로 RestClient(동기) 또는 WebClient(비동기)를 사용해 실제 HTTP 요청을 수행하는 구조로 동작합니다.
- 클라이언트가 API 요청 시 이를 받아서 외부 API 호출을 하여 요청 값을 전달하고 응답값을 반환해 오는 Controller입니다.
package com.adjh.springboot4init.controller;
import com.adjh.springboot4init.service.EchoCallerService;
import com.adjh.springboot4init.service.client.EchoService;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 클라이언트의 요청 값을 통해 외부 API로 요청 값을 전달하고 결과를 응답하는 Controller
*
* @author : leejonghoon
* @fileName : EchoController
* @since : 26. 1. 15.
*/
@RestController
@RequestMapping("/api/echo")
public class EchoController {
private final EchoService echoService;
public EchoController(EchoService echoService) {
this.echoService = echoService;
}
@PostMapping("/test")
public Map<?, ?> testEcho(@RequestBody Map<String, String> request) {
return echoService.echo(request);
}
}
3.4. 추가 : EchoCallerService
💡 추가 : EchoCallerService
- HttpExchange를 통해서 외부 통신을 한 이후에 후 처리를 하는 부분 래퍼로 구성하였습니다.
package com.adjh.springboot4init.service;
import com.adjh.springboot4init.service.client.EchoService;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
* HTTP Service Interface Clients 외부 통신 이후 전처리 및 로깅을 처리하는 호출 Wrapper
*
* @author : leejonghoon
* @fileName : EchoCallerService
* @since : 26. 1. 20.
*/
@Service
public class EchoCallerService {
private final EchoService echoService;
public EchoCallerService(EchoService echoService) {
this.echoService = echoService;
}
public Map<?, ?> callEcho(Map<String, String> message) {
return echoService.echo(message);
}
}
3.5. 정상적으로 외부 호출이 됨을 확인하였습니다.
4) API Versioning
💡 API Versioning
- 기존 Spring Boot 3까지는 /api/v1/users, /api/v2/users와 같이 URL Path 기반 버저닝을 사용하는 경우가 일반적이었습니다. 이 방식은 직관적이지만, 버전이 늘어날수록 Controller와 엔드포인트가 증가하여 라우팅 구조가 복잡해지고 유지보수가 어려워지는 단점이 있습니다.
- Spring Boot 4 (Spring Framework 6.x 이후)에서는 요청 Header를 조건으로 사용하는 API 버저닝 방식을 보다 자연스럽게 적용할 수 있습니다. - 예를 들어, API 호출 시 API-Version 또는 X-API-Version과 같은 Header 값을 전달하고 해당 값에 따라 서로 다른 Controller 메서드를 매핑할 수 있습니다. - 이를 통해 URL 구조를 단순하게 유지하면서도 버전별 API를 명확하게 분리할 수 있어, 확장성과 유지보수 측면에서 더 유연한 설계를 할 수 있습니다.
- URL이나 파라미터가 아니라 HTTP Header 값으로 API 버전을 구분하는 방식입니다. - Spring MVC에게 API 버전을 API-Version 헤더를 기준으로 버전을 확인하는 방식을 의미합니다.
💡 Request Header 내에 API-Version 속성에 대해서 값을 1, 2, 3으로 전달하여서 버전을 확인하는 방식을 의미합니다.
### Echo Test
POST http://localhost:8080/api/echo/test
Content-Type: application/json
API-Version: 1
💡 아래의 코드에서도 useRequestHeader("API-Version")를 통해서 header의 속성을 지정하고 확인하도록 하였습니다.
- API 내에서는 @XXMapping의 속성 값인 version에 따라서 분기를 수행하였습니다. API 내의 [POST] /api/echo/test로 호출을 할 때 Header의 “API-Version” 속성 내에 “1”, “2”로 분기를 하였을 때 각기 다른 API가 호출이 됩니다.
// 설정 API 구성
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureApiVersioning(ApiVersionConfigurer configurer) {
configurer.useRequestHeader("API-Version");
}
}
@RestController
@RequestMapping(value = "/api/echo")
public class EchoController {
private final EchoService echoService;
public EchoController(EchoService echoService) {
this.echoService = echoService;
}
@PostMapping(value = "/test", version = "1")
public Map<?, ?> testEcho(@RequestBody Map<String, String> request) {
System.out.println("버전 1 API가 호출이 되었습니다.");
return echoService.echo(request);
}
@PostMapping(value = "/test", version = "2")
public Map<?, ?> testEcho2(@RequestBody Map<String, String> request) {
System.out.println("버전 2 API가 호출이 되었습니다.");
return echoService.echo(request);
}
}
💡 결과 확인
- 아래와 같이 동일한 API 내에 Header를 API-Version: 1 또는 API-Version: 2로 변경하였을 때, 각기 다른 API가 호출이 됩니다.
2. Client Support
💡 Client Support
- 클라이언트 입장에서 API 버전을 지정하여 요청을 수행해야 할 수 있습니다. 이를 위해 ApiVersionInserter요청에 버전을 한 번만 삽입하는 방법을 정의하는 설정이 있으며, 이후 요청을 할 때는 버전 값만 지정하면 됩니다.
2.1. RestClient 방식
💡 RestClient 방식
- RestClient를 이용하여서 외부 API를 호출할 때에도, 전달받는 API내에서도 .apiVersionInserter()를 통해서 header 내에 API-Version을 전달하도록 구성할 수 있습니다.
- HTTP Service Interface Clients를 이용하여서 외부 API를 호출할 때도, 버전 관리를 지원합니다.
@HttpExchange("/accounts")
public interface AccountService {
@GetExchange(url = "/{id}", version = "1.1")
Account getAccount(@PathVariable int id);
}
5) JmsClient
💡 JmsClient
- JMS(Java Message Service) 내에서 기존 JmsTemplate, JmsMessagingTemplate를 사용했던 부분에 대해서 Spring에서 RestClient / HttpService Clients 스타일에 맞춘 JmsClient API가 추가되었다고 합니다. - 기존에 사용하고 있는 JmsTemplate, JmsMessagingTemplate는 유지가 된다고 합니다.
- JMS(Java Message Service)는 자바 애플리케이션 간 비동기 메시지 통신을 위한 표준 API입니다. 직접적인 메서드 호출(RPC, REST)과 달리 메시지를 매개로 시스템을 느슨하게 결합시키는 것이 핵심 목적입니다. 메시지 큐(메시징 시스템)를 자바에서 사용하기 위한 표준 인터페이스(API)입니다.
- 서로 다른 애플리케이션 또는 컴포넌트가 메시지(Message)를 통해 비동기적으로 통신하도록 지원합니다.
- Message Queue의 경우는 비동기 메시징 구조를 의미하며, JMS의 경우는 이러한 메시지 큐를 사용하기 위한 ‘표준 인터페이스(API 규격)’을 의미합니다. 특히, 메시지 큐 종류 중에서도 JMS 계열인 ActiveMQ, IBM MQ의 경우는 JMS를 이용합니다.
- JmsTemplate은 JMS를 사용하기 위한 Spring의 핵심 템플릿으로, Connection, Session, MessageProducer 등의 생성·관리와 메시지 전송 과정을 자동으로 처리해 주는 도구입니다.
// 템플릿 구성(ConnectionFactory)
@Bean
public ConnectionFactory connectionFactory() {
ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory();
cf.setBrokerURL("tcp://localhost:61616");
cf.setUserName("admin");
cf.setPassword("admin");
return cf;
}
// 메시지 송신 측
@Service
public class OrderProducer {
private final JmsTemplate jmsTemplate;
public OrderProducer(JmsTemplate jmsTemplate) {
this.jmsTemplate = jmsTemplate;
}
public void sendOrder(String orderId) {
jmsTemplate.convertAndSend(
"queue.order",
"주문 ID = " + orderId
);
}
}
// 메시지 수신 측
@Component
public class OrderConsumer {
@JmsListener(destination = "queue.order")
public void receive(String message) {
System.out.println("수신 메시지: " + message);
}
}
3. JmsMessagingTemplate
💡 JmsMessagingTemplate
- Spring이 제공하는 JMS 전용 메시지 전송 헬퍼로, JmsTemplate을 감싸서 메시지를 객체 기반으로 쉽게 보내도록 도와주는 추상화 계층입니다.
// 송신측
@Service
public class PaymentProducer {
private final JmsMessagingTemplate jmsMessagingTemplate;
public PaymentProducer(JmsMessagingTemplate jmsMessagingTemplate) {
this.jmsMessagingTemplate = jmsMessagingTemplate;
}
public void sendPayment(String paymentId) {
jmsMessagingTemplate.convertAndSend(
"queue.payment",
paymentId
);
}
}
// 수신 측
@Component
public class PaymentConsumer {
@JmsListener(destination = "queue.payment")
public void receive(
String payload,
@Header("traceId") String traceId
) {
System.out.println("payload = " + payload);
System.out.println("traceId = " + traceId);
}
}
4. JmsClient
💡 JmsClient - 기존 JmsTemplate, JmsMessagingTemplate와 다르게 플루언트 인터페이스(Fluent Interface) 형태로 구성되어 있습니다. - 소스코드의 가독성을 높이기 위한 목적으로 사용되며 인터페이스 안에 도메인 특화 언어(DSL)를 이용하여 작성합니다.
@Service
public class OrderProducer {
private final JmsClient jmsClient;
public OrderProducer(JmsClient jmsClient) {
this.jmsClient = jmsClient;
}
public void sendOrder(String orderId) {
jmsClient.send("queue.order")
.withBody("주문 ID = " + orderId);
}
}
@Service
public class OrderReceiver {
private final JmsClient jmsClient;
public OrderReceiver(JmsClient jmsClient) {
this.jmsClient = jmsClient;
}
public String receive() {
return jmsClient.receive("queue.order", String.class);
}
}
- OpenTelemetry(OTel)는 관측성(Observability)을 위한 표준 스펙 + 라이브러리 집합이며, 내부 동작에 대해서 관측합니다. - 일반적으로 ‘Trace’를 생성하고 ‘Metric’을 수집합니다. 그리고 ‘Log’로 수집된 데이터를 OTLP로 전송하는 구조를 가집니다.
- OTLP(OpenTelemetry Protocol)는 OpenTelemetry에서 정의한 표준 텔레메트리 전송 프로토콜입니다. - 이는 애플리케이션에서 수집한 관측 데이터(Observability data)를 외부 시스템으로 일관된 방식으로 전달하기 위해 만들어졌습니다. - 수집한 관측 데이터를 Jaeger, Tempo, Prometheus, Datadog, New Relic로 전송을 합니다.
💡 Spring Boot 내에서 전송을 하는 경우 OpenTelemetry Collector를 통해서 각각의 값을 OTLP을 통해서 전송을 합니다
- Metric, Trace, Logs를 각각에 맞게 수집이 되고 Grafana를 통해서 시각화를 수행합니다.