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

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은 deprecated 예정이라 하고, WebClient는 쓰려면 reactive 스택이 필요하고, OpenFeign은 서드파티라 버전 관리가 따로다.

Boot 4는 HTTP Service Clients 자동 구성으로 이 상황을 정리했다. 인터페이스에 @HttpExchange 붙이면 Spring이 구현체를 만들어준다. OpenFeign이 수년간 인기를 끌었던 이유 그 자체를 Spring이 흡수한 것이다.

📄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를 직접 쓰는 게 낫다.

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에 묶여 있었다면 이번 기회에 정리할 만하다.

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 핵심 내용을 매주 이메일로 보내드립니다.