bedaily.me
Backend2026년 5월 7일8분 읽기

Spring Boot 4.0 업그레이드 전에 알아야 할 것들

Spring Boot 4.0은 추가된 기능보다 제거된 것들이 더 중요합니다. 업그레이드 전에 반드시 확인해야 할 Breaking Changes와 새로운 표준을 실코드와 함께 정리합니다.

Spring Boot 4Jakarta EE 11OpenTelemetry마이그레이션Spring Framework 7

개요

Spring Boot 4.0 릴리즈 노트를 처음 훑었을 때 가장 먼저 눈에 들어온 건 새 기능이 아니었다. "Removed"와 "Deprecated" 섹션이었다.

spring-retry 제거. Jackson 2 deprecated. ZooKeeper 퇴출. Java 17 최소 요건.

Spring Boot가 이렇게 많은 걸 한 버전에서 정리한 적이 없었다. 추가된 것들도 의미 있지만, 이 포스트는 왜 이런 결정들이 내려졌는지와 업그레이드 전에 반드시 확인해야 할 것들에 집중한다.

Jakarta EE 11 — Boot 3 때 시작한 전환이 여기서 완성된다

Spring Boot 3에서 javax.jakarta.로 바꾸느라 고생한 기억이 있을 것이다. 솔직히 그건 이름만 바꾼 것이었다. 패키지명이 달라졌을 뿐 API 자체는 EE 9/10 수준에 머물렀다.

Boot 4는 Jakarta EE 11 기반이다. Servlet 6.1, JPA 3.2, Bean Validation 3.1. 숫자가 올라간 것보다 중요한 건, 이제 EE 11에서 실질적으로 추가된 API를 활용할 수 있다는 점이다.

예를 들어 JPA 3.2에서 find()에 락 옵션을 직접 넘길 수 있게 됐다. 비관적 락이 필요할 때 기존처럼 별도 @Query를 작성할 필요가 없다.

📄OrderService.java
// 기존: 비관적 락을 위해 레포지토리에 별도 메서드 정의 필요
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT o FROM Order o WHERE o.id = :id")
Optional<Order> findByIdForUpdate(Long id);

// JPA 3.2: find()에 직접 옵션 전달
Order order = em.find(Order.class, orderId, LockModeType.PESSIMISTIC_WRITE);

EE 11 호환성 때문에 서드파티 라이브러리(특히 JPA 구현체, Validation 관련)는 버전을 꼭 확인하자. Hibernate는 7.1로 올라갔다.

HTTP Service Clients — 드디어 공식 답이 나왔다

Spring에서 외부 API를 호출할 때마다 선택지가 많아서 오히려 불편했다. RestTemplate은 유지보수 모드(신규 기능 없음)라 하고, WebClient는 쓰려면 reactive 스택이 필요하고, OpenFeign은 서드파티라 버전 관리가 따로다.

Boot 4는 HTTP Service Clients 자동 구성으로 이 상황을 정리했다. 인터페이스에 @HttpExchange 붙이면 Spring이 구현체를 만들어준다. OpenFeign이 인기를 끌었던 건 HTTP 호출을 인터페이스 하나로 선언하면 구현체를 프레임워크가 알아서 만들어주는 선언적 방식 덕분이었는데, Spring이 이 패턴을 공식으로 흡수한 것이다.

RestTemplate유지보수 모드WebClientreactive 스택 필요OpenFeign서드파티@HttpExchange선언적 인터페이스 · Boot 4 자동구성스트리밍·대용량 멀티파트는 여전히 WebClient 직접 사용
흩어져 있던 HTTP 클라이언트 선택지를 @HttpExchange 선언적 인터페이스로 통합 — 스트리밍·멀티파트는 여전히 WebClient 직접 사용
📄UserApiClient.java
@HttpExchange("https://api.example.com")
public interface UserApiClient {

    @GetExchange("/users/{id}")
    User getUser(@PathVariable Long id);

    @PostExchange("/users")
    User createUser(@RequestBody CreateUserRequest request);
}
📄UserApiClientConfig.java
@Bean
UserApiClient userApiClient(RestClient.Builder builder) {
    var restClient = builder.baseUrl("https://api.example.com").build();
    return HttpServiceProxyFactory
        .builderFor(RestClientAdapter.create(restClient))
        .build()
        .createClient(UserApiClient.class);
}

@HttpExchange는 사실 Spring Framework 6.0부터 있었다. Boot 4에서 달라진 건 auto-configuration이 완성돼 공식 권장 패턴으로 자리 잡았다는 것이다. 다만 스트리밍이나 멀티파트 같은 케이스는 여전히 WebClient를 직접 쓰는 게 낫다. @HttpExchange의 선언적 인터페이스로는 청크 단위로 흘러오는 응답을 논블로킹으로 소비하거나, 대용량 파일을 메모리에 다 올리지 않고 리액티브 스트림으로 전송하는 세밀한 제어가 어렵기 때문이다.

Observability — 설정 세 곳이 하나로

클라우드 환경에서 메트릭, 트레이스, 로그를 각각 따로 설정하는 게 당연했다. Micrometer로 메트릭, Zipkin이나 Jaeger로 분산 트레이싱, 로그는 Logback 따로. 같은 요청 하나를 추적하려면 세 곳을 넘나들어야 했다.

Boot 4는 spring-boot-starter-opentelemetry를 추가했다. 의존성 하나에 OTLP 기반 메트릭·트레이스·로그가 묶인다.

📄build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-opentelemetry'
}
📄application.yml
management:
  otlp:
    metrics:
      export:
        url: http://otel-collector:4318/v1/metrics
    tracing:
      endpoint: http://otel-collector:4318/v1/traces
  tracing:
    sampling:
      probability: 1.0

OTLP를 지원하는 벡엔드라면 어디든 연결된다. Grafana Tempo, Jaeger, Datadog, Honeycomb 등 인프라 선택이 자유로워진다. 기존에 Zipkin에 묶여 있었다면 이번 기회에 정리할 만하다.

관측 가능성(Observability)과 그 입력이 되는 텔레메트리 데이터, 전송 표준 OTLP를 함께 정리하면 다음과 같습니다.

Observability(관측 가능성)란?

Observability(관측 가능성) — 시스템의 외부 출력만 보고 내부 상태를 얼마나 잘 파악할 수 있는지를 나타내는 속성. "장애가 났을 때 미리 정의해두지 않은 질문에도 답할 수 있는가"가 핵심이다. 모니터링이 "미리 정한 지표를 지켜보는 것"이라면, Observability는 "예상 못한 문제의 원인까지 파고들 수 있는 능력"에 가깝다. 이를 가능하게 하는 입력이 바로 텔레메트리 데이터다.

텔레메트리 데이터란?

텔레메트리 데이터 — 실행 중인 시스템이 자기 상태를 외부에서 관찰할 수 있도록 내보내는 측정·관측 데이터. 어원은 그리스어 tele(원격) + metron(측정)으로, 소프트웨어에서는 애플리케이션이 자기 동작 상태를 바깥으로 보고하는 데이터를 가리킨다.

세 가지 종류 (세 기둥)

  • Traces — 요청 하나가 여러 서비스를 거쳐가는 전체 경로와 각 구간(span) 소요 시간. 예: 입찰 조회 요청이 Gateway → 서비스 → ElasticSearch → PostgreSQL을 거치는 흐름. MSA에서 어느 구간이 느린지 찾을 때 핵심.
  • Metrics — 시간에 따라 집계되는 수치. 응답시간 백분위수(p99), 처리량, CPU 사용률, Kafka 컨슈머 래그 등. 추세 관찰에 사용.
  • Logs — 특정 시점에 발생한 이벤트 기록. 에러 스택 트레이스, 디버그 메시지 등.

활용 흐름 — 메트릭으로 이상을 감지하고 → 트레이스로 느린 구간을 좁히고 → 로그로 원인을 확인하는 식으로 셋을 함께 쓴다.

OTLP(OpenTelemetry Protocol)란?

OTLP(OpenTelemetry Protocol) — 위 텔레메트리 데이터를 표준 포맷으로 실어 나르는 전송 프로토콜. OpenTelemetry가 세 종류를 모두 하나의 프로토콜로 다룬다는 점이 핵심 가치다.

  • 역할 — SDK에서 수집한 데이터를 Collector나 백엔드(Jaeger, Tempo, Prometheus 등)로 보내는 공통 와이어 포맷. 벤더별로 제각각이던 전송 방식을 통일.
  • 데이터 모델 — traces, metrics, logs 세 시그널을 하나로 처리하며, 페이로드는 Protocol Buffers(protobuf)로 정의.
  • 전송 방식 — gRPC 기반(기본 포트 4317, 서버 간 통신에 유리)과 HTTP 기반(기본 포트 4318, 방화벽·브라우저 환경에 유용) 두 가지.
  • 계층 구조 — 애플리케이션(OTel SDK) → OTLP → Collector → 백엔드 스토리지 흐름. Collector를 중간에 두면 가공·배치·라우팅을 한 곳에서 처리.
  • Spring Boot 연동 — micrometer-tracing + opentelemetry-exporter-otlp 조합으로 트레이스를 내보내는 것이 일반적. 메트릭도 OTLP registry로 같은 방식 전송 가능.

API 버전 관리 내장

REST API 버전 관리 방식은 팀마다 달랐다. URL에 /v1/ 넣는 팀, 헤더로 처리하는 팀, Accept 미디어 타입으로 구분하는 팀. 표준이 없으니 새 팀원이 오면 항상 설명이 필요했다.

Boot 4는 프레임워크 레벨에서 버전 관리 전략을 설정으로 통일할 수 있게 했다.

📄application.yml
spring:
  mvc:
    apiversion:
      default-version: "1"
      header-name: "X-API-Version"
📄UserController.java
@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping("/{id}")
    @RequestMapping(version = "1")
    public UserV1Response getUserV1(@PathVariable Long id) { ... }

    @GetMapping("/{id}")
    @RequestMapping(version = "2")
    public UserV2Response getUserV2(@PathVariable Long id) { ... }
}

클라이언트는 X-API-Version: 2 헤더를 보내면 된다. 헤더 없이 오면 default-version으로 처리한다.

무엇이 사라졌나

spring-retry 제거 → @Retryable / @ConcurrencyLimit

build.gradlespring-retry 의존성을 따로 추가하던 게 이제 필요 없다. @Retryable@ConcurrencyLimit이 Spring Core에 들어왔다.

📄ExternalApiService.java
@Service
public class ExternalApiService {

    @Retryable(retryFor = ApiException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000))
    public String callExternalApi() {
        return restClient.get().uri("/data").retrieve().body(String.class);
    }

    // 동시 실행을 10개로 제한 — 외부 API 쓰로틀링 대응
    @ConcurrencyLimit(10)
    public void processItem(Item item) { ... }
}

애너테이션은 동일하게 쓸 수 있다. 기존 spring-retry를 쓰고 있었다면 의존성만 제거하면 되는 경우가 대부분이다.

Jackson 2 deprecated → Jackson 3

이게 가장 번거로운 변경이다. Jackson 3은 Java 17+ 기준으로 재설계됐고, 일부 모듈의 패키지 구조가 바뀌었다.

Boot의 auto-configuration을 그대로 쓰고 있다면 크게 손댈 게 없다. 문제는 ObjectMapper를 직접 빈으로 등록하거나 커스텀 직렬화 로직이 있는 경우다.

📄JacksonConfig.java
// Jackson 2 방식 — 동작은 하지만 deprecated 경고 발생
@Bean
public ObjectMapper objectMapper() {
    return new ObjectMapper()
        .registerModule(new JavaTimeModule())
        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}

// Jackson 3 — 모듈 등록 방식은 동일, import 경로 일부 변경
// com.fasterxml.jackson.datatype.jsr310 → tools.jackson.datatype.jsr310 (확인 필요)

마이그레이션 전에 프로젝트 내 com.fasterxml.jackson import를 검색해서 직접 사용하는 부분을 파악해두자.

ZooKeeper 퇴출 → Kafka KRaft

Kafka 4.0에서 ZooKeeper 없이 동작하는 KRaft 모드가 완성됐다. Boot 4는 EmbeddedKafkaZKBroker를 제거하고 EmbeddedKafkaKraftBroker만 남겼다.

📄KafkaIntegrationTest.java
@SpringBootTest
@EmbeddedKafka(partitions = 1, kraft = true)
class OrderEventListenerTest {

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    @Test
    void shouldProcessOrderEvent() {
        kafkaTemplate.send("order-events", "order-1");
        // ...
    }
}

프로덕션 Kafka가 이미 KRaft 모드라면 테스트 설정만 kraft = true로 바꾸면 된다. 아직 ZooKeeper 기반이라면 Kafka 클러스터 마이그레이션도 함께 계획에 넣자.

Spring Boot 3.x → 4.0 마이그레이션 체크리스트

  • Java 버전: 최소 17이지만 Virtual Threads 제대로 쓰려면 21 이상 권장
  • Jakarta EE 11: 서드파티 JPA·Validation 라이브러리 버전 확인 (Hibernate 7.1+)
  • Jackson: Auto-configuration만 쓴다면 이상 없음. 직접 ObjectMapper 등록 코드는 수정 필요
  • spring-retry: 의존성 제거, @Retryable / @ConcurrencyLimit으로 전환
  • Kafka: EmbeddedKafkaZKBroker 사용 테스트 → kraft = true로 수정
  • 공식 마이그레이션 가이드 필수 확인 — 이 포스트에서 다루지 않은 변경사항이 더 있다

마무리

업그레이드가 쉽진 않다. Jackson 마이그레이션도 있고, Kafka 설정도 봐야 하고, EE 11 호환성도 체크해야 한다. 그래도 이 변경들이 향하는 방향은 일관된다.

Spring Boot 4는 Java 8/11 시대에 만들어진 것들을 정리하고 Java 21+ 환경에 맞게 다시 쌓겠다는 선언이다. 마이그레이션 비용은 한 번이지만, 그 이후엔 더 가볍고 관찰 가능한 코드베이스가 기다린다.

주간 기술 뉴스레터

Backend · AI · Java 핵심 내용을 매주 이메일로 보내드립니다.