Day 24. Spring AI 2.0 마이그레이션 실전
안녕하세요, 여러분의 AI 튜터 홍순구입니다.
지난 시간에 Day 1부터 Day 23까지 쌓아온 모든 축을 회고하고, Spring AI 2.0의 주요 변경점을 카테고리별로 정리했어요.
오늘은 그 변경점들을 직접 코드로 옮겨요.
build.gradle 버전 올리기부터 시작해서, OpenRewrite 자동 변환, 수동 보정, 전체 테스트 Green까지 — ai-friends를 Spring AI 1.1.0에서 2.0으로 실제 마이그레이션합니다.
⚠️ 버전 안내: Spring AI 2.0.0 GA는 2026년 6월 4일 출시 예정이에요.
RC1(6/1) 이후로는 API가 동결되니까, RC1 이상 버전으로 진행하면 안전해요. 오늘 강의의 Before/After 코드는 M1~RC1 공식 업그레이드 노트를 기반으로 합니다.
🎯 오늘의 한 줄. ai-friends 코드베이스를 Spring AI 1.1.0 → 2.0으로 마이그레이션하고, 전체 테스트 Green을 달성한다.
🎯 학습 목표
- Spring Boot 3.5.x → 4.0.x + Spring AI 1.1.0 → 2.0.0 빌드 기반을 전환한다
- OpenRewrite 레시피로 MCP 패키지 이동을 자동화한다
- ChatClient tool API 통합, ChatMemory ID 필수화를 수동 보정한다
- Usage API 리네임과 Document builder 패턴으로 전환한다
- MCP 클라이언트 설정을 2.0 구조로 재구성한다
- 전체
./gradlew testGreen을 달성하고 마이그레이션 회고를 정리한다
Step 1. 마이그레이션 전략 + 빌드 기반 전환
Big Picture — 무엇이 바뀌는가
Day 23 Step 5에서 정리한 변경 카테고리를 다시 한번 볼게요. 오늘은 이걸 코드로 실행해요.
| 카테고리 | 요약 | 오늘 Step |
|---|---|---|
| 빌드 기반 | SB 4.0 + Jackson 3 + AI 2.0 | Step 1 |
| ChatClient + Tool API | 자동 등록 + 메서드 통합 | Step 2 |
| ChatMemory + 설정 | ID 필수화 + .options 제거 | Step 3 |
| MCP 마이그레이션 | 패키지 이동 + SDK 2.0 | Step 4 |
| Usage + Document | API 리네임 + builder 패턴 | Step 5 |
| 전체 빌드 Green | 테스트 통과 + 회고 | Step 6 |
마이그레이션 전략: Big Bang
Day 23 생각해볼 주제에서 Big Bang vs 점진 전환을 논의했어요. ai-friends 규모(365파일, 37테스트)에서는 Big Bang이 적합해요.
실행 순서는 이래요.
1. build.gradle 버전 올리기 (이 시점에서 컴파일 에러 폭발)
2. OpenRewrite 자동 변환 (MCP 패키지 이동 자동화)
3. 수동 보정 (ChatClient, ChatMemory, Usage 등)
4. ./gradlew test → Red → 하나씩 Green 으로
5. 전체 Green 달성
build.gradle 버전 전환
현재 ai-friends의 build.gradle이에요.
// build.gradle — Before (1.1.x)
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.14'
id 'io.spring.dependency-management' version '1.1.7'
}
ext {
set('springAiVersion', '1.1.0')
}
2.0으로 올리면 이렇게 바뀌어요.
// build.gradle — After (2.0)
plugins {
id 'java'
id 'org.springframework.boot' version '4.0.0'
id 'io.spring.dependency-management' version '1.1.7'
}
ext {
set('springAiVersion', '2.0.0')
}
Spring Boot 4.0이 가져오는 변화:
| 축 | 3.5.x | 4.0.x |
|---|---|---|
| Spring Framework | 6.x | 7.x |
| Jackson | 2.x (com.fasterxml.jackson) |
3.x (tools.jackson) |
| Java 최소 | 17 | 17 (21 권장) |
| Null Safety | 선택적 | 기본 적용 |
Jackson 3의 패키지 변경이 가장 충격적이에요. com.fasterxml.jackson → tools.jackson으로 바뀌었어요.
우리 ai-friends에서 Jackson을 직접 import한 곳이 있다면 전부 수정해야 해요.
다행히 Spring AI가 내부적으로 Jackson을 감싸고 있어서, 우리 비즈니스 코드에서 직접 Jackson을 건드리는 곳은 많지 않아요.
Resilience4j 아티팩트 전환
Day 20에서 추가한 Resilience4j도 Boot 4.0용 아티팩트로 바꿔야 해요.
// Before
implementation 'io.github.resilience4j:resilience4j-spring-boot3'
// After
implementation 'io.github.resilience4j:resilience4j-spring-boot4'
💡 결론: 빌드 기반 전환은 "버전 숫자만 바꾸기"처럼 보이지만, Spring Framework 7 + Jackson 3이라는 큰 기반 이동이 뒤따라요.
이 시점에서 컴파일 에러가 터지는 건 정상이에요. 이제부터 하나씩 잡아갈 거예요.
Step 2. ChatClient API + Tool Calling 통합
Tool Calling 자동 등록 (M7)
2.0에서는 ChatClient에 tools를 설정하면 ToolCallAdvisor가 자동 등록돼요. 이전에는 수동으로 추가해야 했어요.
// Before (1.1.x) — 수동 등록 필요
chatClient.prompt("날씨 알려줘")
.tools(weatherTool)
.advisors(ToolCallAdvisor.builder().build())
.call().content();
// After (2.0) — 자동 등록
chatClient.prompt("날씨 알려줘")
.tools(weatherTool)
.call().content();
ai-friends에서 ToolCallAdvisor를 수동으로 추가한 곳을 찾아서 제거하면 돼요. 자동 등록을 끄고 싶다면 명시적으로 비활성화할 수 있어요.
// 자동 등록 비활성화 (수동 제어가 필요할 때)
chatClient.prompt("날씨 알려줘")
.tools(weatherTool)
.advisors(
AdvisorParams.toolCallAdvisorAutoRegister(false),
ToolCallAdvisor.builder().build())
.call().content();
Tool 메서드 통합 (M7)
개별 tool 메서드가 하나로 통합됐어요.
// Before (1.1.x) — 개별 메서드
chatClient.prompt()
.toolCallbacks(myCallback)
.toolContext(Map.of("tenantId", "acme"))
.call().content();
// After (2.0) — 통합 API
chatClient.prompt()
.tools(t -> t.callbacks(myCallback)
.context("tenantId", "acme"))
.call().content();
toolCallbacks(), toolContext(), toolNames() 모두 deprecated 되고 tools() 하나로 통합됐어요.
SpringBeanToolCallbackResolver 폐기 (M7)
이전에는 Function 빈을 @Bean으로 등록하면 자동으로 Tool로 인식했어요.
// Before (1.1.x) — bare Function 빈
@Bean
@Description("Get the weather in location")
Function<WeatherRequest, WeatherResponse> currentWeather() {
return weatherService::getWeather;
}
// After (2.0) — 명시적 ToolCallback 빈
@Bean
ToolCallback currentWeather() {
return FunctionToolCallback
.builder("currentWeather", weatherService::getWeather)
.description("Get the weather in location")
.inputType(WeatherRequest.class)
.build();
}
ai-friends에서 @Tool 어노테이션을 사용한 곳은 그대로 동작해요. Function 빈으로 등록한 곳만 ToolCallback 빈으로 전환하면 돼요.
🙋 학생 질문 — "기존 @Tool 어노테이션은 안 바꿔도 되나요?"
네, @Tool 어노테이션은 2.0에서도 그대로 동작해요.
바뀐 건 ChatClient의 tool 등록 API(toolCallbacks → tools)와 Function 빈 자동 해석(SpringBeanToolCallbackResolver 폐기)이에요. Day 11에서 만든 @Tool 함수들은 그대로 써도 돼요.
💡 결론: Tool Calling의 핵심 변화는 자동 등록 + 메서드 통합이에요. 수동으로 ToolCallAdvisor를 추가하던 코드를 제거하고, toolCallbacks() → tools() 로 통합하면 돼요.
Step 3. ChatMemory 변경 + 설정 프로퍼티
Conversation ID 필수화 (M6)
가장 영향이 큰 변경 중 하나예요. ChatMemory.DEFAULT_CONVERSATION_ID 상수가 삭제됐어요. 매 호출마다 conversation ID를 명시적으로 전달해야 해요.
ai-friends의 SoulmateChatService를 보면 이미 conversation ID를 전달하고 있어요.
// SoulmateChatService.java — 현재 코드 (1.1.x)
chatClient.prompt()
.user(userMessage)
.advisors(advisor -> advisor.param(
ChatMemory.CONVERSATION_ID, conversationId))
.call().content();
이 코드는 2.0에서도 그대로 동작해요. Day 5에서 처음 ChatMemory를 도입할 때부터 conversation ID를 명시적으로 전달하는 패턴을 사용했기 때문이에요.
⚠️ 만약 DEFAULT_CONVERSATION_ID에 의존하던 코드가 있다면 수정이 필요해요.
// Before (1.1.x) — 기본값 사용
String id = ChatMemory.DEFAULT_CONVERSATION_ID;
// After (2.0) — 명시적 ID 생성
String id = UUID.randomUUID().toString();
PromptChatMemoryAdvisor 삭제 (M6)
PromptChatMemoryAdvisor가 삭제되고 MessageChatMemoryAdvisor로 통합됐어요. ai-friends는 처음부터 MessageChatMemoryAdvisor를 사용하고 있어서 영향이 없어요.
conversationId() 빌더 메서드 삭제 (M6)
// Before (1.1.x) — 빌더에서 설정
MessageChatMemoryAdvisor.builder(chatMemory)
.conversationId("my-session")
.build();
// After (2.0) — 호출 시점에 param으로 전달
MessageChatMemoryAdvisor.builder(chatMemory).build();
chatClient.prompt()
.advisors(a -> a.param(
ChatMemory.CONVERSATION_ID, "my-session"))
.call().content();
설정 프로퍼티 .options 제거 (M6)
application.yml에서 .options. 세그먼트가 제거됐어요.
# Before (1.1.x)
spring:
ai:
openai:
chat:
options:
model: gpt-4
temperature: 0.7
# After (2.0)
spring:
ai:
openai:
chat:
model: gpt-4
temperature: 0.7
하위 호환을 위해 .options. 프로퍼티도 당분간 동작하지만 deprecated 경고가 뜰 거예요.
ai-friends의 application.yml에서 .options.을 사용하는 곳을 전부 찾아서 제거하면 돼요.
Observation 스팬 이름 변경 (M6)
Day 22 Grafana 대시보드의 쿼리에 영향이 있어요.
Before: tool_call <tool-name>
After: execute_tool <tool-name>
spring-ai-overview.json 대시보드 파일에서 메트릭 이름을 업데이트해야 해요.
🙋 학생 질문 — "application.yml의 .options를 안 지우면 어떻게 되나요?"
당장은 동작해요. Spring AI 2.0은 하위 호환을 위해 .options. 프로퍼티를 아직 읽어요.
하지만 로그에 deprecated 경고가 뜨고, 2.1이나 3.0에서 완전 제거될 수 있어요. 마이그레이션하는 김에 깔끔하게 제거하는 게 좋아요.
💡 결론: ChatMemory는 Day 5에서 올바른 패턴(명시적 ID 전달)을 사용했기 때문에 영향이 작아요.
설정 프로퍼티의 .options. 제거는 단순 작업이지만 빠뜨리기 쉬우니 grep으로 전수 확인하세요.
Step 4. MCP 마이그레이션
MCP 어노테이션 패키지 이동 (M3)
MCP 어노테이션이 외부 라이브러리에서 Spring AI 본체로 흡수됐어요.
// Before (1.1.x — 외부 라이브러리)
import org.springaicommunity.mcp.annotation.McpTool;
import org.springaicommunity.mcp.provider.tool.SyncMcpToolProvider;
// After (2.0 — Spring AI 내장)
import org.springframework.ai.mcp.annotation.McpTool;
import org.springframework.ai.mcp.annotation.provider.tool.SyncMcpToolProvider;
이 변경은 OpenRewrite로 자동화할 수 있어요.
# OpenRewrite 자동 변환 실행
./gradlew rewrite \
-Drewrite.activeRecipes=\
org.springframework.ai.migration.M3MigrateMcpAnnotations
MCP Transport 모듈 이동 (M3)
MCP transport 의존성의 그룹 ID가 바뀌었어요.
// build.gradle — Before
implementation 'io.modelcontextprotocol.sdk:mcp-spring-webflux'
// build.gradle — After
implementation 'org.springframework.ai:mcp-spring-webflux'
Java 패키지도 함께 바뀌었어요.
// Before
import io.modelcontextprotocol.server.transport
.WebFluxSseServerTransportProvider;
// After
import org.springframework.ai.mcp.server.webflux.transport
.WebFluxSseServerTransportProvider;
이것도 OpenRewrite 레시피로 자동화돼요.
./gradlew rewrite \
-Drewrite.activeRecipes=\
org.springframework.ai.migration.M3MigrateMcpSpringTransports
MCP Client Customizer 통합 (M3)
// Before (1.1.x)
@Bean
public McpSyncClientCustomizer mySyncCustomizer() {
return (name, spec) ->
spec.requestTimeout(Duration.ofSeconds(30));
}
// After (2.0)
@Bean
public McpClientCustomizer<McpClient.SyncSpec> mySyncCustomizer() {
return (name, spec) ->
spec.requestTimeout(Duration.ofSeconds(30));
}
MCP SDK 2.0 변경 (M5)
MCP Java SDK가 2.0으로 올라가면서 몇 가지가 바뀌었어요.
Tool 입력 스키마 타입 변경:
// Before
McpSchema.JsonSchema schema = tool.inputSchema();
// After
Map<String, Object> schema = tool.inputSchema();
서버 쪽 Tool 입력 검증 기본 활성화: 실패 시 isError=true로 응답이 돌아와요. 비활성화하려면 명시적으로 꺼야 해요.
전체 MCP 마이그레이션 한 번에 실행
OpenRewrite 레시피를 합쳐서 한 번에 실행할 수 있어요.
./gradlew rewrite \
-Drewrite.activeRecipes=\
org.springframework.ai.migration.MigrateToSpringAI200M3
이 한 줄로 MCP 어노테이션 이동, transport 이동, client customizer 통합이 한 번에 적용돼요.
💡 결론: MCP 마이그레이션의 80%는 OpenRewrite로 자동화할 수 있어요. 수동으로 해야 하는 건 MCP SDK 2.0의 타입 변경(JsonSchema → Map)과 설정 블록 재구조화 정도예요.
Step 5. Usage API + Document builder
Usage API 리네임
ai-friends의 UsageTrackingAdvisor와 UsageTrackingMeterAdvisor에서 토큰 사용량을 읽는 코드예요.
// UsageTrackingAdvisor.java — Before (1.1.x)
long prompt = safeInt(usage.getPromptTokens());
long completion = safeInt(usage.getCompletionTokens());
// UsageTrackingMeterAdvisor.java — Before (1.1.x)
long prompt = safeValue(usage.getPromptTokens());
long completion = safeValue(usage.getCompletionTokens());
2.0에서 메서드 이름이 바뀌고 반환 타입도 바뀔 수 있어요.
// After (2.0) — 예상 변경
long prompt = usage.getInputTokens(); // Integer → Long
long completion = usage.getOutputTokens(); // Integer → Long
⚠️ 이 리네임은 공식 업그레이드 노트에 아직 명시되지 않았어요. RC1/GA 시점에 실제 API를 확인한 뒤 적용하세요. 만약 리네임이 없다면 기존 코드 그대로 동작해요.
UsageBudgetAdvisor도 동일 패턴
Day 19에서 만든 UsageBudgetAdvisor와 Day 22의 CostAlertHealthIndicator에서도 같은 메서드를 사용해요. Usage API가 바뀌면 이 클래스들도 함께 수정해요.
Document builder 패턴
new Document() 생성자 호출을 builder 패턴으로 전환할 수 있어요.
// Before (1.1.x)
Document doc = new Document(text, metadata);
// After (2.0) — builder 패턴
Document doc = Document.builder()
.text(text)
.metadata(metadata)
.build();
ai-friends에서 new Document()를 사용하는 곳을 찾아볼게요. 주로 RAG 관련 코드(Day 15~16)에 있어요.
⚠️ 생성자가 완전 삭제(builder-only)되었는지는 RC1/GA에서 확인이 필요해요. 생성자가 deprecated만 되었다면 기존 코드도 당분간 동작해요.
JsonParser → JsonHelper (RC1)
JsonParser가 deprecated 되고 JsonHelper로 대체됐어요.
// Before (1.1.x)
JsonParser.fromJson(json, MyType.class);
JsonParser.toJson(object);
JsonParser.getJsonMapper();
// After (2.0)
JsonHelper jsonHelper = new JsonHelper();
jsonHelper.fromJson(json, MyType.class);
jsonHelper.toJson(object);
JacksonUtils.getDefaultJsonMapper();
ai-friends에서 JsonParser를 직접 사용하는 곳이 있다면 JsonHelper로 바꿔요.
🙋 학생 질문 — "확정 안 된 변경을 미리 준비하는 게 의미가 있나요?"
의미가 있어요. 마이그레이션의 핵심은 "어디를 건드려야 하는지 파악하는 것"이에요.
실제 리네임이 발생하든 안 하든, "Usage API를 사용하는 곳이 4곳이고, 변경이 필요하면 이 패턴으로 바꾸면 된다"는 것을 파악해두면 GA 출시 후 30분 만에 마이그레이션을 끝낼 수 있어요.
💡 결론: Usage API와 Document builder는 "미리 대상 파악 + GA 확인 후 즉시 적용" 전략이 효율적이에요. grep으로 대상을 미리 찾아두고, RC1/GA 릴리즈 노트를 확인한 뒤 일괄 적용하면 돼요.
Step 6. 전체 빌드 Green + 마이그레이션 회고
빌드 순서 정리
모든 수정이 끝났으면, 빌드와 테스트를 돌려볼게요.
# 1. 컴파일 확인
./gradlew compileJava
# 2. 테스트 실행
./gradlew test
# 3. 전체 빌드
./gradlew build
처음에는 컴파일 에러가 나올 수 있어요. 가장 흔한 에러 유형은 이래요.
| 에러 유형 | 원인 | 해결 |
|---|---|---|
cannot find symbol: DEFAULT_CONVERSATION_ID |
M6 삭제 | 명시적 ID 생성으로 대체 |
cannot find symbol: toolCallbacks |
M7 deprecated → 삭제 | tools() 로 통합 |
package does not exist: org.springaicommunity |
M3 패키지 이동 | OpenRewrite 또는 수동 import 수정 |
incompatible types: JsonSchema vs Map |
M5 SDK 2.0 | 타입 변경 |
com.fasterxml.jackson not found |
Boot 4.0 Jackson 3 | tools.jackson 으로 패키지 수정 |
마이그레이션 체크리스트
전체 Green을 달성하기 위한 체크리스트예요. 하나씩 체크해가면서 진행하세요.
| # | 항목 | Step | 확인 |
|---|---|---|---|
| 1 | build.gradle 버전 전환 (SB 4.0 + AI 2.0) | 1 | ☐ |
| 2 | Resilience4j 아티팩트 전환 (boot3 → boot4) | 1 | ☐ |
| 3 | ToolCallAdvisor 수동 등록 제거 | 2 | ☐ |
| 4 | toolCallbacks() → tools() 통합 | 2 | ☐ |
| 5 | DEFAULT_CONVERSATION_ID 참조 제거 | 3 | ☐ |
| 6 | .options. 프로퍼티 제거 | 3 | ☐ |
| 7 | Observation 스팬 이름 업데이트 | 3 | ☐ |
| 8 | OpenRewrite MCP 자동 변환 실행 | 4 | ☐ |
| 9 | MCP 수동 보정 (SDK 2.0 타입 변경) | 4 | ☐ |
| 10 | Usage API 리네임 (RC1/GA 확인 후) | 5 | ☐ |
| 11 | Document builder 전환 (RC1/GA 확인 후) | 5 | ☐ |
| 12 | JsonParser → JsonHelper | 5 | ☐ |
| 13 | Jackson 3 import 수정 | 전체 | ☐ |
| 14 | ./gradlew test 전체 Green | 6 | ☐ |
마이그레이션 회고 — 가장 까다로운 지점
실제 마이그레이션을 해보면, 이런 순서로 어려움이 느껴져요.
1순위: Jackson 3 패키지 변경
com.fasterxml.jackson → tools.jackson은 Spring AI와 무관하게 Spring Boot 4.0 자체의 변경이에요. 직접 Jackson 클래스를 import한 곳이 많으면 가장 큰 작업이 돼요.
2순위: MCP 패키지 이동
MCP 관련 import가 3곳(어노테이션, transport, client)에서 동시에 바뀌어요. OpenRewrite가 대부분 잡아주지만, 수동 설정(application.yml)은 직접 수정해야 해요.
3순위: Tool API 통합
toolCallbacks(), toolContext(), toolNames()를 tools() 하나로 바꾸는 건 기계적이지만, 호출 지점이 여러 곳에 퍼져 있으면 빠뜨리기 쉬워요.
가장 쉬운 것: ChatMemory
Day 5에서부터 올바른 패턴(명시적 ID 전달)을 사용했기 때문에, DEFAULT_CONVERSATION_ID 삭제의 영향이 거의 없어요. 강의를 제대로 따라왔다면 여기서 에러가 0건이에요.
Day 1 ~ Day 24 완전 종료
Day 1에서 RestClient로 Gemini를 호출하고 "Hello, AI"를 받아본 그 순간부터, Day 24에서 2.0 마이그레이션까지.
24일 동안 ai-friends는 이렇게 진화했어요.
Day 0 : RestClient + 수동 JSON 파싱
Day 2 : ChatClient + 프로바이더 추상화
Day 5 : JdbcChatMemoryRepository + 영속 대화
Day 11 : @Tool + 에이전트의 시작
Day 14 : 가드레일 + Agentic Patterns
Day 15 : pgvector + RAG
Day 18 : MCP + A2A
Day 22 : Prometheus + Grafana + PII 마스킹
Day 23 : LLM Ops 조감 + 전체 회고
Day 24 : Spring AI 2.0 마이그레이션 완료
Spring AI의 원리를 이해하고 있으니, 버전이 바뀌어도 적응할 수 있어요.
브랜치 세이브
git add -A
git commit -m "feat: Spring AI 2.0 migration complete (Day 24)"
# 브랜치: day24-spring-ai-2.0
중간 합류 학생 가이드:
git checkout day23-llm-ops # 1.1.x 최종 상태
# 또는
git checkout day24-spring-ai-2.0 # 2.0 마이그레이션 완료 상태
cp .env.example .env
./run.sh
마무리
오늘 Day 24에서 다룬 내용을 정리할게요.
| Step | 한 줄 요약 |
|---|---|
| 1 | build.gradle 버전 전환 (SB 4.0 + AI 2.0 + Jackson 3) |
| 2 | ChatClient tool API 통합 + 자동 등록 |
| 3 | ChatMemory ID 필수화 + .options 프로퍼티 제거 |
| 4 | MCP 패키지 이동 (OpenRewrite 자동 + 수동 보정) |
| 5 | Usage API 리네임 + Document builder |
| 6 | 전체 빌드 Green + 마이그레이션 회고 |
24일간의 여정을 마치며
Day 1에서 시작해서 Day 24까지 왔어요. Spring AI의 기초부터 에이전트 패턴, RAG, MCP, Harness, Observability, LLM Ops, 그리고 2.0 마이그레이션까지.
여러분이 가져가는 건 코드만이 아니에요. LLM 앱을 설계하고, 구현하고, 운영하고, 진화시키는 감각이에요. 이 감각이 있으면 어떤 프레임워크가 나와도 적응할 수 있어요.
마지막까지 함께해 주셔서 고마워요. 여러분의 AI 엔지니어링 여정을 응원합니다.
생각해볼 주제
1. OpenRewrite 자동 변환의 한계는 어디까지인가?
OpenRewrite는 MCP 패키지 이동 같은 기계적 변환을 자동화해줘요.
하지만 "ChatMemory ID를 어떤 값으로 채울지", "Tool API 통합 시 context를 어떻게 전달할지" 같은 비즈니스 판단은 자동화할 수 없어요.
자동 변환과 수동 보정의 경계를 어디에 두는 것이 효율적인지 생각해 보세요.
2. 마이그레이션 중 "중간 배포"를 해야 한다면 어떻게 할 것인가?
Big Bang 마이그레이션 중에 긴급 핫픽스가 필요하면 어떻게 하나요?
마이그레이션 브랜치와 메인 브랜치가 분기된 상태에서 핫픽스를 양쪽에 모두 적용해야 하는 상황이 올 수 있어요. 이런 시나리오를 미리 어떻게 대비할지 생각해 보세요.
3. Spring AI 3.0이 나오면 또 마이그레이션해야 하는가?
메이저 버전 업그레이드는 반복되는 일이에요. 마이그레이션 비용을 줄이려면 프레임워크 API에 얼마나 직접 의존하는지가 핵심이에요.
우리 ai-friends의 Advisor 패턴이 "프레임워크 직접 의존"인지 "추상화로 감싸는 것"인지, 3.0 대비를 위해 지금 바꿀 것이 있는지 생각해 보세요.
✅ 예시 답안정답 보기
Day 24는 hands-on 마이그레이션으로, 과제 없이 생각해볼 주제 3개만 제공합니다.
생각해볼 주제 1. OpenRewrite 자동 변환의 한계는 어디까지인가?
[문제 상황 요약]
OpenRewrite는 MCP 패키지 이동 같은 기계적 변환을 자동화해줘요. 하지만 모든 마이그레이션을 자동화할 수 있는 건 아니에요.
자동 변환과 수동 보정의 경계를 어디에 두는 것이 효율적인지가 핵심 질문이에요.
[튜터의 가이드 및 해설]
자동화할 수 있는 것 (기계적 변환):
| 변환 유형 | 예시 | 이유 |
|---|---|---|
| import 문 변경 | org.springaicommunity → org.springframework.ai |
1:1 매핑, 모호함 없음 |
| 패키지 이동 | MCP transport 그룹 ID 변경 | 이름만 바뀜 |
| 메서드 리네임 | internalCall() → call() |
시그니처 동일 |
| deprecated 제거 | 사용되지 않는 import 정리 | 단순 삭제 |
자동화할 수 없는 것 (비즈니스 판단 필요):
| 변환 유형 | 예시 | 이유 |
|---|---|---|
| 기본값 선택 | DEFAULT_CONVERSATION_ID 삭제 후 어떤 ID를 쓸지 |
비즈니스 컨텍스트에 따라 다름 |
| API 통합 | toolCallbacks() + toolContext() → tools() 합치기 |
인자 조합이 호출마다 다름 |
| 설정 재구조화 | MCP application.yml 블록 |
구조가 근본적으로 변경 |
| 테스트 수정 | Mock 객체의 메서드 이름 변경 | 테스트 의도 파악 필요 |
실무 전략: 80/20 규칙
마이그레이션 대상의 80%는 기계적 변환이에요. OpenRewrite로 이 80%를 30분 만에 처리하고, 나머지 20%에 집중하는 게 효율적이에요.
중요한 건 자동 변환 후 반드시 diff를 검토하는 거예요. OpenRewrite가 의도하지 않은 변환을 할 수 있어요. 특히 동명이인 클래스(다른 패키지의 같은 이름)가 잘못 매핑되는 경우가 있어요.
🎯 면접관을 홀리는 핵심 멘트
"OpenRewrite는 import 변경, 패키지 이동 같은 기계적 변환의 80%를 자동화합니다. 하지만 '삭제된 기본값을 무엇으로 대체할지', 'API 통합 시 인자를 어떻게 조합할지' 같은 비즈니스 판단은 사람이 해야 해요. 실무에서는 OpenRewrite로 반복 작업을 빠르게 처리한 뒤, diff를 꼼꼼히 검토하고 수동 보정에 집중하는 80/20 전략이 효과적입니다."
생각해볼 주제 2. 마이그레이션 중 "중간 배포"를 해야 한다면?
[문제 상황 요약]
Big Bang 마이그레이션을 진행 중인데, 메인 브랜치에 긴급 핫픽스가 필요한 상황이에요. 마이그레이션 브랜치와 메인 브랜치가 분기된 상태에서 핫픽스를 양쪽에 모두 적용해야 해요.
[튜터의 가이드 및 해설]
시나리오: 마이그레이션 50% 진행 중, 프로덕션에서 PII 마스킹 버그 발견
main (1.1.x) ── hotfix 필요
\
migration (2.0 작업 중) ── 50% 완료
전략 1: 메인 브랜치에서 핫픽스 → Cherry-pick
# 메인에서 핫픽스
git checkout main
git checkout -b hotfix/pii-masking
# 수정 후
git checkout main && git merge hotfix/pii-masking
# 마이그레이션 브랜치에 cherry-pick
git checkout migration
git cherry-pick <hotfix-commit>
# 충돌 해결 (2.0 API로 적응)
장점: 프로덕션 배포가 빠름. 단점: cherry-pick 시 2.0 API와 충돌 가능.
전략 2: Feature Flag로 사전 방어
마이그레이션 시작 전에 주요 기능에 Feature Flag를 걸어두면, 마이그레이션 중에도 메인 브랜치에서 기능을 빠르게 끄고 켤 수 있어요.
if (featureFlags.isEnabled("pii-masking-v2")) {
// 새 로직
} else {
// 기존 로직 (긴급 시 여기로 전환)
}
전략 3: 마이그레이션 기간 최소화
가장 확실한 방어는 마이그레이션 기간을 최대한 짧게 가져가는 거예요.
| 프로젝트 규모 | 권장 마이그레이션 기간 |
|---|---|
| 소규모 (ai-friends) | 1일 (3시간) |
| 중규모 (수십 모듈) | 1~2주 |
| 대규모 (수백 모듈) | 1~2개월 + Feature Flag |
ai-friends 규모에서는 3시간이면 끝나니까, 핫픽스 리스크가 사실상 없어요. 대규모 프로젝트에서는 Feature Flag + 점진 전환이 필수예요.
🎯 면접관을 홀리는 핵심 멘트
"마이그레이션 중 핫픽스 대응은 '마이그레이션 기간 최소화'가 최선의 방어입니다. 소규모 프로젝트는 Big Bang으로 1일 안에 끝내면 리스크가 거의 없어요. 대규모에서는 Feature Flag를 사전에 걸어두고, 메인 브랜치 핫픽스 → 마이그레이션 브랜치 cherry-pick 워크플로를 준비합니다. 핵심은 '분기 상태가 길어질수록 충돌 비용이 기하급수적으로 증가한다'는 거예요."
생각해볼 주제 3. Spring AI 3.0이 나오면 또 마이그레이션해야 하는가?
[문제 상황 요약]
메이저 버전 업그레이드는 반복되는 일이에요. 마이그레이션 비용을 줄이려면 프레임워크 API에 얼마나 직접 의존하는지가 핵심이에요.
[튜터의 가이드 및 해설]
ai-friends의 프레임워크 의존도 분석:
| 계층 | 프레임워크 직접 의존 | 3.0 영향 예상 |
|---|---|---|
| Controller | ChatClient 직접 사용 |
높음 |
| Service | ChatClient + Advisor |
높음 |
| Advisor | CallAroundAdvisor 구현 |
중간 |
| Config | application.yml 직접 참조 |
높음 |
| Domain | 프레임워크 의존 없음 | 없음 |
마이그레이션 비용을 줄이는 3가지 전략:
1. 얇은 래퍼 (Thin Wrapper)
ChatClient를 직접 사용하는 대신, 우리 인터페이스로 한 번 감싸면 프레임워크 변경이 래퍼에만 영향을 미쳐요.
// 프레임워크 직접 의존
public String chat(String message) {
return chatClient.prompt().user(message)
.call().content();
}
// 래퍼로 감싸기
public interface AiChatPort {
String chat(String message);
}
public class SpringAiChatAdapter implements AiChatPort {
public String chat(String message) {
return chatClient.prompt().user(message)
.call().content();
}
}
3.0에서 API가 바뀌면 SpringAiChatAdapter만 수정하면 돼요.
2. 테스트 커버리지
Day 21의 AgentBench처럼 행동 기반 테스트를 충분히 갖추면, 마이그레이션 후 "기존 동작이 깨졌는지"를 빠르게 확인할 수 있어요. API가 바뀌어도 테스트가 Green이면 안심할 수 있어요.
3. 업그레이드 노트 구독
Spring AI GitHub Releases를 Watch하면, M1 단계부터 breaking change를 추적할 수 있어요. GA까지 기다리지 말고 M1~M3 시점에 영향도를 파악해두면 준비 시간이 생겨요.
현실적인 답: "적정 수준의 추상화"
모든 것을 래퍼로 감싸면 오버엔지니어링이에요. ai-friends 규모에서는 ChatClient를 직접 사용하는 게 오히려 읽기 좋아요.
현실적인 선은 이래요.
- Domain 계층: 프레임워크 독립 유지 (이미 달성)
- Service 계층: ChatClient 직접 사용 OK (규모가 작으니까)
- Config 계층:
application.yml변경은 어차피 피할 수 없음 - 테스트: AgentBench + 통합 테스트로 회귀 감지
🎯 면접관을 홀리는 핵심 멘트
"메이저 버전 마이그레이션 비용을 줄이는 핵심은 '적정 수준의 추상화 + 충분한 테스트 커버리지'입니다. 모든 API를 래퍼로 감싸는 건 오버엔지니어링이지만, Domain 계층은 프레임워크 독립으로 유지하고, 행동 기반 테스트(AgentBench)로 회귀를 빠르게 감지하는 것이 실용적이에요. 그리고 M1 단계부터 breaking change를 추적하면 GA 시점에 이미 준비가 끝나 있습니다."