MOM.SOFT

MOMSOFT  /  기술 블로그

Spring · Java

Spring Boot 2에서 3으로, 실제로 시간을 잡아먹는 것들

패키지 이름 치환은 도구가 대신 해 줍니다. 나머지 전부가 진짜 일입니다. 되돌릴 수 있는 단위로 끊어 올리는 순서를 정리했습니다.

프로젝트 문의하기 →

결론부터: 어려운 건 jakarta가 아닙니다

Spring Boot 2에서 3으로 올리는 작업을 견적 내면 대부분 javaxjakarta로 바꾸는 일을 가장 크게 잡습니다. 실제로는 그게 가장 쉽습니다. 기계적이고, 도구가 있고, 컴파일러가 남은 곳을 거의 전부 알려줍니다. 코드베이스가 아무리 지저분해도 이 부분은 끝이 보이는 작업입니다.

시간을 잡아먹는 건 세 가지입니다. 첫째, Spring Security 설정을 사실상 새로 쓰는 일. 문법 치환이 아니라 설정 모델 자체가 바뀌었기 때문에, 기존 설정이 무엇을 열고 무엇을 막고 있었는지 다시 확인해야 합니다. 둘째, Hibernate 6에서 조용히 달라지는 동작. 컴파일도 되고 애플리케이션도 뜨는데 쿼리 결과 타입이나 생성되는 DDL이 달라집니다. 셋째, 서드파티 호환 대기. 이건 개발 문제가 아니라 일정 문제이고, 팀이 손쓸 수 없는 유일한 항목이라 프로젝트의 임계 경로가 됩니다.

그래서 이 글의 권고는 하나입니다. 한 번에 올리지 마십시오. 여러 단계로 끊고, 각 단계가 그 자체로 배포 가능하고 되돌릴 수 있게 설계하십시오. 장수 브랜치를 만들어 마지막 날 머지하는 방식은, 마이그레이션 자체보다 그 브랜치를 유지하는 데 더 많은 비용이 듭니다.

왜 이 업그레이드만 유독 다른가

Spring Boot의 마이너 업그레이드는 대개 점진적입니다. 낡은 API에 @Deprecated가 붙고, 한동안 둘 다 동작하고, 다음 버전에서 사라집니다. 중간 상태가 허용되기 때문에 조금씩 옮길 수 있습니다.

Boot 3는 이 성질이 깨지는 지점이 하나 있습니다. Jakarta EE 네임스페이스 전환에는 중간 상태가 없습니다. javax.servlet.Filterjakarta.servlet.Filter는 이름이 비슷할 뿐 아무 관계도 없는 별개의 타입입니다. 서블릿 컨테이너, 프레임워크, 애플리케이션 코드, 그리고 모든 서드파티 라이브러리가 동시에 같은 쪽에 있어야 합니다. 한 라이브러리라도 구 네임스페이스에 남아 있으면 그 지점에서 NoClassDefFoundErrorClassNotFoundException이 납니다. 그것도 컴파일 시점이 아니라, 그 코드 경로를 처음 타는 런타임에 납니다.

여기에 전제 조건이 몇 개 더 붙습니다. Boot 3는 Java 17 이상을 요구하고, 내부적으로 Spring Framework 6와 Hibernate 6를 쓰며, 내장 컨테이너도 Jakarta EE 9 이상 기반(내장 Tomcat은 10.1 계열)으로 올라갑니다. 즉 런타임 · 언어 레벨 · ORM · 웹 계층이 한꺼번에 바뀝니다. 이 네 가지를 한 커밋에 묶으면, 무언가 깨졌을 때 원인이 넷 중 어디인지 특정하는 데만 하루가 갑니다. 끊어야 하는 이유가 여기 있습니다.

덧붙여, 이건 선택이 아니라 시한이 있는 작업입니다. Spring Boot 2.x는 오픈소스 지원이 종료되어 보안 패치가 공개 릴리스로 나오지 않고, 상용 지원 역시 무기한이 아닙니다. 정확한 종료 시점은 공식 지원 정책 페이지에서 확인하십시오. "지금 급하지 않으니 나중에"는 성립하지만, "안 해도 된다"는 성립하지 않습니다.

javax → jakarta: 바뀌는 것과 바뀌지 않는 것

가장 흔한 실수는 프로젝트 전체에 javax.jakarta. 일괄 치환을 돌리는 것입니다. 모든 javax가 Jakarta EE 소속은 아닙니다. 상당수는 JDK가 소유한 패키지라서 바꾸면 그 자리에서 컴파일이 깨집니다.

바꿔야 하는 것 (Jakarta EE)그대로 두는 것 (JDK 또는 별개 소속)
javax.servlet.*javax.sql.* (DataSource 등)
javax.persistence.*javax.crypto.*
javax.validation.*javax.naming.*
javax.annotation.PostConstruct / PreDestroy / Resourcejavax.net.ssl.*
javax.transaction.Transactionaljavax.imageio.*, javax.swing.*
javax.mail.*, javax.jms.*, javax.xml.bind.*javax.annotation.Nullable (JSR-305 계열)

특히 javax.annotation 패키지는 함정입니다. 같은 패키지 이름 아래에 Jakarta로 넘어간 애너테이션(@PostConstruct)과 넘어가지 않은 정적 분석용 애너테이션(@Nullable)이 섞여 있습니다. 일괄 치환하면 후자가 조용히 깨집니다.

손으로 정규식을 돌리기보다는 도구를 쓰는 편이 낫습니다. OpenRewrite에는 Jakarta 전환과 Boot 3 업그레이드 레시피가 있고, 애너테이션 · import · 의존성 좌표를 함께 정리해 줍니다. 다만 도구를 신뢰하되 검증은 하십시오. 자동 변환이 잘 못 잡는 곳은 정해져 있습니다.

전환이 끝났는지 확인할 때는 소스가 아니라 빌드된 산출물을 봐야 합니다. 소스에는 없어도 의존성 JAR 안에 남아 있을 수 있기 때문입니다. Boot 실행 가능 JAR은 의존성이 BOOT-INF/lib 아래에 중첩돼 있어 그대로는 훑히지 않으니, 풀어 놓고 봅니다.

unzip -q -o build/libs/app.jar -d /tmp/app

# 애플리케이션 클래스에 남은 참조
jdeps -verbose:class /tmp/app/BOOT-INF/classes 2>/dev/null \
  | grep -E 'javax\.(servlet|persistence|validation|transaction)'

# 구 네임스페이스를 참조하는 의존성 JAR 찾기
for j in /tmp/app/BOOT-INF/lib/*.jar; do
  jdeps -verbose:class "$j" 2>/dev/null \
    | grep -qE 'javax\.(servlet|persistence|validation|transaction)' && echo "$j"
done

이건 1차 스크리닝일 뿐입니다. 리플렉션이나 문자열로 클래스를 찾는 코드는 바이트코드 의존성으로 잡히지 않습니다. 그래서 다음 항목이 필요합니다.

Spring Security: 치환이 아니라 재작성입니다

여기가 가장 많은 시간을 씁니다. WebSecurityConfigurerAdapter가 사라지면서 설정 방식이 상속 기반에서 빈 등록 기반으로 바뀌었고, 인가 판단도 AuthorizationManager 기반으로 정리됐습니다.

바뀐 것들

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf.disable())          // 토큰 기반 무상태 API에 한해서
        .sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/public/**").permitAll()
            .requestMatchers("/api/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated()      // 반드시 명시할 것
        )
        .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
    return http.build();
}

예제의 csrf.disable()을 습관적으로 복사하지 마십시오. 세션 쿠키로 인증하는 화면이 하나라도 남아 있다면 CSRF를 끄는 순간 구멍입니다. 무상태 토큰 API에만 해당하는 설정이고, 혼합 구조라면 경로별로 필터 체인을 나누는 편이 낫습니다.

실제로 사고가 나는 지점

매처 통합이 조용한 구멍을 만듭니다. requestMatchers("/api/**")처럼 문자열만 넘기면, 스프링이 MVC 매처와 Ant 매처 중 무엇으로 해석할지 스스로 고릅니다. 서블릿이 여러 개 매핑돼 있거나 spring.mvc.servlet.path를 바꿔 둔 애플리케이션에서는 판단이 모호해져 기동 자체가 실패하기도 합니다. 이건 그나마 나은 경우입니다. 더 위험한 쪽은 정상 기동하면서 의미만 달라지는 경우입니다. 예전에 antMatchersmvcMatchers를 섞어 썼다면 둘의 확장자 · 후행 슬래시 처리 규칙이 달랐다는 점을 기억하고, 통합 후 각 경로가 여전히 같은 요청 집합을 가리키는지 확인해야 합니다. server.servlet.context-path를 쓰는 애플리케이션은 특히 그렇습니다.

후행 슬래시 매칭이 더 이상 공짜가 아닙니다. /orders에 매핑된 핸들러가 /orders/ 요청도 자동으로 받아 주던 동작이 기본에서 빠지는 방향으로 정리됐습니다(관련 옵션은 deprecated 되었고, 정확한 기본값 전환 시점은 사용하는 버전의 릴리스 노트를 확인하십시오). 외부 클라이언트가 슬래시를 붙여 호출하고 있었다면 업그레이드 직후 404가 쏟아집니다. 판단 기준은 단순합니다. 호출자를 전부 통제하지 못한다면 서버에서 흡수하는 쪽입니다. 업그레이드 전에 접근 로그에서 슬래시로 끝나는 요청이 실제로 얼마나 오는지 세어 보십시오.

규칙에 걸리지 않는 요청의 기본 동작을 반드시 명시하십시오. anyRequest()를 빼먹은 설정은 의도와 정반대로 동작할 수 있습니다. 그리고 이런 종류의 실수는 테스트가 없으면 절대 드러나지 않습니다.

그래서 순서상 중요한 결론이 하나 나옵니다. 시큐리티 설정을 고치기 전에, 경로별 접근 권한 테스트를 먼저 쓰십시오. "인증 없이 /api/admin/users를 호출하면 401, USER 권한이면 403, ADMIN이면 200" 수준의 MockMvc 테스트 한 묶음이면 됩니다. 이 테스트는 Boot 2 상태에서 통과해야 하고, 재작성 후에도 그대로 통과해야 합니다. 마이그레이션에서 회귀를 잡아 주는 안전망 중 투자 대비 효과가 가장 큰 항목입니다. 여기에 더해 /actuator/mappings 출력을 업그레이드 전후로 덤프해 diff를 뜨면, 사라지거나 형태가 바뀐 매핑이 한눈에 드러납니다.

덧붙여, 새 스타일 자체는 Boot 3 전용이 아닙니다. SecurityFilterChain 빈과 람다 DSL은 Boot 2.7이 함께 쓰는 Spring Security 5.7 라인에서 이미 지원하고(WebSecurityConfigurerAdapter는 이 시점에 deprecated 됐습니다), 매처 통합도 5.x 후반 라인에 백포트되어 있습니다. 즉 이 재작성은 Boot 3로 올리기 전에 미리 끝내 두고 따로 배포할 수 있습니다. 뒤에서 설명할 단계 분리의 핵심입니다. 어느 API가 어느 마이너 버전부터 가능한지는 사용하는 라인의 마이그레이션 가이드로 확인하십시오.

Hibernate 6: 컴파일은 되는데 동작이 다릅니다

Hibernate 6는 타입 처리와 쿼리 해석 · SQL 생성 계층을 다시 썼습니다. 그래서 문제가 컴파일 에러로 나오지 않고 런타임 동작 차이로 나옵니다. 마이그레이션에서 가장 늦게 발견되고 가장 아픈 부류입니다.

먼저 터지는 것 (컴파일 · 기동 실패)

조용히 다른 것 (진짜 위험한 쪽)

확인 방법

가장 확실한 검증은 추측이 아니라 산출물 대조입니다. ddl-auto를 건드려 DB를 바꾸는 방식이 아니라, JPA의 스크립트 생성 기능으로 파일만 뽑아서 비교합니다.

  1. 업그레이드 전, Boot 2에서 spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create...scripts.create-target=ddl-boot2.sql생성 DDL을 파일로 덤프합니다.
  2. 업그레이드 후 같은 작업을 합니다. 단, JPA 3에서는 프로퍼티 접두사가 jakarta.persistence.schema-generation...으로 바뀌었습니다. 여기서 이름을 그대로 두면 아무것도 생성되지 않고, 그걸 "차이 없음"으로 오해하기 쉽습니다.
  3. 두 파일로 diff를 뜹니다. 컬럼명 · 타입 · 시퀀스 · 제약조건 차이가 여기서 전부 드러납니다.
  4. 운영 스키마에는 ddl-auto: validate를 고정합니다. 마이그레이션 기간에 Hibernate가 스키마를 손대게 두는 건 최악의 조합입니다.
  5. 네이티브 쿼리와 @Query는 목록을 뽑아 전수 확인합니다. 리포지토리 테스트가 없다면 여기만이라도 Testcontainers로 실제 DB를 띄워 테스트를 붙이는 게 값어치를 합니다. 인메모리 DB로는 타입 해석 차이가 재현되지 않습니다.

Spring Data JPA 쪽도 함께 정리됩니다. getOne 계열은 사라졌으니 getReferenceById를 쓰고, 네이티브 쿼리에 Pageable을 붙였다면 countQuery를 명시하십시오. 자동 유도에 기대던 코드가 버전에 따라 예외로 바뀌는 경우가 있습니다. 리포지토리 인터페이스를 훑으며 한 번에 정리하는 편이 낫습니다.

서드파티 호환 대기: 유일하게 통제 불가능한 항목

앞의 세 가지는 팀이 노력하면 끝납니다. 이것만은 아닙니다. 그래서 가장 먼저 조사해야 합니다. 코드를 한 줄도 고치기 전에, 착수 여부를 판단하는 재료로 씁니다.

재고 조사

의존성 트리를 뽑아 구 네임스페이스에 묶인 아티팩트를 전부 나열합니다.

# Gradle
./gradlew dependencies --configuration runtimeClasspath > deps.txt
# Maven
mvn dependency:tree -Dverbose > deps.txt

그리고 각 항목을 네 부류로 나눕니다.

부류대응비용
Jakarta 지원 상위 버전이 이미 나와 있음버전만 올린다낮음
같은 버전에 jakarta 분류자/좌표를 별도 제공좌표를 바꾼다 (QueryDSL의 jakarta 분류자가 대표적)낮음
지원 계획 없음, 대체재 있음교체 — 코드 수정이 따라온다중간~높음
벤더 제공 바이너리, 소스 없음문의 후 대기, 또는 격리통제 불가

국내 프로젝트에서 마지막 부류에 걸리는 것들은 대체로 정해져 있습니다. 리포트 엔진, 전자결재 · 문서변환 모듈, 결제 · 인증 SDK, 보안 솔루션 에이전트, 오래된 사내 공통 라이브러리입니다. 이들은 대부분 javax.servlet에 직접 의존합니다. 벤더에 문의할 때는 "지원 예정인가"가 아니라 "Jakarta 대응 버전의 출시 시점을 문서로 받을 수 있는가"를 물으십시오. 답이 없으면 그 자체가 판단 재료입니다.

사내 공통 라이브러리는 별도 항목입니다

사내에서 만든 스타터나 공통 모듈이 있다면 네임스페이스 외에 하나가 더 걸립니다. Boot 3는 META-INF/spring.factories의 자동 설정 등록 키를 더 이상 읽지 않습니다. META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports로 옮겨야 합니다. 이 증상은 에러 없이 자동 설정만 사라지는 형태로 나타나서, "빈이 없다"는 엉뚱한 지점에서 발견됩니다. 새 방식은 Boot 2.7에서 이미 지원하므로 미리 옮겨 둘 수 있습니다. 같은 맥락에서 @ConstructorBinding도 패키지와 사용 위치 규칙이 바뀌었으니 설정 프로퍼티 클래스를 함께 훑으십시오.

마지막 수단과 그 대가

Eclipse Transformer 계열 도구로 JAR 바이트코드를 변환할 수 있습니다. 다만 대가를 알고 쓰십시오. 서명된 JAR은 서명이 깨지고, 문자열로 클래스명을 조립해 리플렉션으로 로딩하는 코드는 변환되지 않으며, 무엇보다 벤더 지원 범위를 벗어난 바이너리가 됩니다. 장애가 났을 때 "저희가 배포한 파일이 아닙니다"라는 답을 받게 됩니다. 임시방편으로는 유효하지만 영구 해법으로 삼지 마십시오.

더 나은 선택지는 격리입니다. 문제 라이브러리를 쓰는 기능만 별도 프로세스로 떼어 Boot 2에 남겨 두고 HTTP나 메시지로 호출하는 방식입니다. 모놀리스 전체를 인질로 잡히는 것보다 낫고, 벤더가 대응하면 그 모듈만 흡수하면 됩니다.

같이 걸리는 흔한 것들

끊어서 올리는 순서

아래 각 단계는 독립적으로 배포 가능하고, 이전 단계로 되돌릴 수 있어야 합니다. 단계 사이에 실제 운영 배포를 한 번씩 끼워 넣는 것이 핵심입니다. 브랜치에 쌓아 두고 마지막에 한 번에 올리면 단계를 나눈 의미가 없습니다.

0단계 — 조사와 안전망 (배포 없음)

1단계 — Java 17 (Boot 2 유지)

런타임과 언어 레벨만 바꿉니다. 빌드 도구 · 애너테이션 프로세서 · 에이전트(APM, 보안 에이전트)를 Java 17 지원 버전으로 올리고, 컨테이너 베이스 이미지를 교체합니다. 이 단계에서 GC 로그와 메모리 프로파일을 반드시 확인하십시오. 특히 Java 8에서 올라오는 경우 기본 GC가 Parallel에서 G1으로 바뀌므로 지연 시간과 힙 사용 곡선의 모양이 달라집니다. 여기서 확인해 두지 않으면, 나중에 Boot 3와 섞였을 때 성능 변화의 원인을 분리할 수 없습니다. 되돌리기: 이전 이미지 태그로 재배포.

2단계 — Boot 2 최신 마이너 + deprecation 청소

Boot 2 라인의 마지막 버전으로 올리고, spring-boot-properties-migrator를 임시로 넣어 프로퍼티 경고를 전부 없앱니다. 확인이 끝나면 이 의존성은 반드시 제거하십시오. 운영에 남겨 두면 기동 시간과 로그만 늘어납니다. 그리고 Spring Security 설정을 새 스타일로 재작성합니다. Boot 2 상태에서 SecurityFilterChain 빈과 람다 DSL을 쓸 수 있으므로, 가장 위험한 변경을 가장 안전한 환경에서 검증할 수 있습니다. 사내 공통 라이브러리의 자동 설정 등록 파일도 이 단계에서 새 형식으로 옮깁니다. 0단계에서 만든 인가 테스트가 여기서 값을 합니다. 되돌리기: 이 단계 커밋만 revert.

3단계 — 서드파티 정리 (Boot 2 유지)

Jakarta 지원 버전이 나와 있는 라이브러리 중 Boot 2에서도 동작하는 것들을 미리 올려 둡니다. 다음 단계에서 한꺼번에 바뀌는 변수의 수를 줄이는 작업입니다.

4단계 — jakarta 전환 + Boot 3 (한 번에)

여기만은 나눌 수 없습니다. 네임스페이스는 원자적이기 때문입니다. 대신 이 단계를 최대한 얇게 만드는 것이 지금까지의 준비였습니다. 자동 변환을 돌리고, 남은 컴파일 에러를 잡고, 리소스 파일과 생성 코드를 갱신합니다. 기능 변경은 절대 섞지 마십시오. 이 브랜치의 diff에 비즈니스 로직 수정이 한 줄이라도 들어가면 리뷰가 무의미해집니다. 되돌리기: 이전 이미지 태그. 가능하면 카나리나 블루-그린으로 소수 인스턴스에 먼저 태우십시오.

5단계 — Hibernate 6 정합성 확인

기술적으로는 4단계에 포함되지만, 확인 작업은 별도로 잡아야 합니다. DDL diff 대조, 네이티브 쿼리 전수 확인, 배치 · 정산 · 통계처럼 하루에 한 번 또는 한 달에 한 번만 도는 코드 경로 점검. 사고는 대개 이 "가끔 도는 코드"에서 납니다. 배포 후 최소 한 번의 월말 배치 주기가 지날 때까지를 관찰 기간으로 잡고, 그 전에는 다음 대형 변경을 시작하지 마십시오.

6단계 — 그 다음

가상 스레드, AOT · 네이티브 이미지, 새 관측 스택 같은 것은 전부 여기부터입니다. 마이그레이션과 섞지 마십시오. 이건 업그레이드가 아니라 새 기능 도입이고, 별도의 판단과 별도의 롤백 계획이 필요합니다.

되돌릴 수 있는 지점 설계 — 실무 규칙

  1. DB 스키마는 마이그레이션 기간에 건드리지 않습니다. 애플리케이션은 되돌릴 수 있어도 스키마는 되돌리기 어렵습니다. 스키마 변경이 필요하면 마이그레이션 전후로 미루십시오.
  2. 브랜치 수명을 짧게 유지합니다. 머지까지 몇 주가 걸린다면 단계를 더 잘게 쪼개야 한다는 신호입니다.
  3. 이전 버전 아티팩트를 즉시 재배포할 수 있게 유지합니다. 이미지 태그를 지우지 말고, 롤백 절차를 실제로 한 번 연습해 보십시오. 연습하지 않은 롤백 절차는 없는 것과 같습니다.
  4. 단계마다 관측을 먼저 확보합니다. 로그 포맷, 트레이스, 헬스체크 엔드포인트가 정상인지 확인한 뒤에 트래픽을 올립니다.
  5. 기능 변경과 절대 섞지 않습니다. "어차피 손대는 김에"가 마이그레이션 실패의 가장 흔한 원인입니다.

언제는 하지 말아야 하는가

이 업그레이드가 항상 옳은 선택은 아닙니다. 다음 경우에는 미루거나, 아예 다른 계획을 세우는 편이 낫습니다.

1. 통제 불가능한 의존성이 임계 경로에 있을 때

핵심 기능이 벤더 제공 바이너리에 묶여 있고 그 벤더가 Jakarta 대응 일정을 제시하지 못한다면, 지금 시작하는 건 끝을 알 수 없는 작업에 인력을 묶는 것입니다. 이 경우 우선순위는 마이그레이션이 아니라 그 의존성을 격리 가능한 모듈로 떼어내는 리팩터링입니다. 그게 끝나면 나머지는 언제든 올릴 수 있습니다.

2. 회귀를 확인할 수단이 전혀 없을 때

테스트도 없고, QA 인력도 없고, 운영 로그로 이상을 감지할 방법도 없다면 업그레이드는 "잘 됐는지 알 수 없는 변경"이 됩니다. Hibernate 6의 조용한 동작 변화는 이런 환경에서 반드시 운영 사고로 나타나고, 그것도 한참 뒤에 나타납니다. 테스트 확보는 선행 작업이지 병행 작업이 아닙니다.

3. 배포 환경이 고정되어 있을 때

WAR로 빌드해 외부 WAS에 올리는 구조라면, 그 WAS가 Jakarta EE 9 이상(Servlet 5.0+)을 지원하는지가 전제 조건입니다. 실무적으로는 Tomcat 10 계열 이상, 또는 각 벤더의 Jakarta EE 9 대응 버전을 뜻합니다. 지원하지 않는다면 Boot 3 애플리케이션은 애초에 배포되지 않습니다. WAS 교체는 대개 인프라 · 계약 · 라이선스가 얽힌 별도 프로젝트입니다. 순서를 뒤집지 마십시오.

4. 다른 대형 변경이 동시에 진행 중일 때

모듈 분리, DB 엔진 교체, 대규모 기능 개편과 병행하면 장애 원인 분석이 불가능해집니다. 마이그레이션은 다른 변경이 조용한 기간에 넣어야 합니다.

5. 수명이 얼마 남지 않은 시스템일 때

가까운 시일 안에 폐기하거나 전면 재작성하기로 이미 결정된 시스템이라면, 업그레이드 비용을 새 시스템에 쓰는 편이 낫습니다. 다만 인터넷에 노출된 시스템이라면 이 판단은 성립하지 않습니다. 지원이 끝난 버전을 외부에 열어 두는 것은 비용 절감이 아니라 위험 이전일 뿐이고, 그 경우엔 폐기 일정을 앞당기는 쪽이 정답입니다.

6. 근거가 "최신이라서"뿐일 때

Java 17 이상의 언어 기능, 관측 스택 현대화, 가상 스레드, 네이티브 이미지 — 이런 것들이 지금 이 시스템에서 어떤 문제를 푸는지 말할 수 없다면 아직 업그레이드할 시점이 아닙니다. 보안 지원 종료는 그 자체로 충분한 근거이지만, 그렇다면 판단해야 할 것은 "어떻게 안전하게 올릴 것인가"이지 "무엇을 더 얹을 것인가"가 아닙니다.

정리

Spring Boot 3 마이그레이션의 난이도는 코드베이스 크기에 비례하지 않습니다. 통제할 수 없는 의존성의 개수회귀를 확인할 수 있는 수단의 유무에 비례합니다. 그래서 착수 전에 해야 할 일은 코드 수정이 아니라 조사와 안전망 구축이고, 착수 후에 해야 할 일은 변경을 되돌릴 수 있는 단위로 끊는 것입니다.

패키지 이름을 바꾸는 건 도구가 해 줍니다. 시큐리티 설정이 실제로 무엇을 막고 있었는지, Hibernate가 만들어 내던 SQL이 어떻게 달라졌는지, 벤더가 언제 대응할 것인지 — 이 세 가지에 대한 답을 가지고 있느냐가 프로젝트의 성패를 가릅니다.

같은 주제의 다른 글

맘소프트는 19년 넘게 금융 · 엔터프라이즈 · 모바일 시스템을 만들어 왔습니다. 여기 적는 글은 그 과정에서 실제로 부딪히고 해결한 것들입니다.

기술 판단이 필요하신가요

맘소프트는 소프트웨어 개발과 CTO 대행 · 기술 자문을 제공합니다. 편하게 문의해 주세요.

[email protected]