Anthropic 모듈을 공식 Java SDK로 마이그레이션하기
Anthropic 모듈을 공식 Java SDK로 마이그레이션하기
2.0.0-M3에서 spring-ai-anthropic은 수제로 만든 RestClient / WebClient 구현을 대체하고 공식 com.anthropic:anthropic-java SDK 기반 위에서 다시 작성됐어요. 이 글에서는 무엇이 바뀌었는지, 어떤 영향이 있는지, 그리고 기존 코드를 새 구조로 마이그레이션하는 방법을 자세히 알아볼게요.
출처: 문서
본문
2.0.0-M3에서 spring-ai-anthropic은 공식 com.anthropic:anthropic-java SDK 기반 위에서 다시 작성되어, 수제로 만든 RestClient / WebClient 구현을 대체했어요. 이전의 AnthropicApi 클래스는 중첩 DTO record 47개를 가진 2,300줄짜리 파일이었죠.
새 모듈은 병렬 API가 아니라 SDK 위의 얇은 어댑터예요. Spring AI의 가치는 ChatModel, ChatClient, 어드바이저, 관측 가능성(observability), 자동 설정 같은 자체 추상화와, 제공자 간에 동작하는 기능에 있어요. SDK가 이미 해당 관심사(cache-control 모델링, 스트리밍, rate-limit 처리)를 다루는 곳에서는, 이전 모듈의 래퍼를 유지하지 않고 버렸기 때문에 애플리케이션은 SDK 타입을 직접 사용해요. 이렇게 함으로써 표면적(surface area)을 작게 유지하고, Anthropic이 새 SDK 릴리스를 출시할 때 드리프트(drift)를 피할 수 있어요.
Maven 좌표, spring-ai-starter-model-anthropic Boot starter, spring.ai.anthropic.* 구성 프로퍼티는 모두 변경되지 않았어요. ChatClient API도 변경되지 않았어요. ChatModel.call(Prompt)와 ChatModel.stream(Prompt)는 서명을 그대로 유지해요. AnthropicChatOptions는 모든 기존 필드를 유지하면서, 스킬(skills), 웹 검색, 서비스 티어, inference geo, 구조화된 출력을 위한 새 필드를 추가해요.
What Changed
| Area | Change |
|---|---|
AnthropicChatModel construction |
Public 생성자 제거. AnthropicChatModel.builder() 사용. |
org.springframework.ai.anthropic.api.AnthropicApi 및 중첩 DTO |
제거. 직접 API 접근은 SDK의 com.anthropic.client.AnthropicClient를 사용. |
AnthropicCacheOptions, AnthropicCacheStrategy, AnthropicCacheTtl, CacheBreakpointTracker, CacheEligibilityResolver |
org.springframework.ai.anthropic.api(및 api.utils)에서 루트 org.springframework.ai.anthropic 패키지로 이동. Enum 값은 변경되지 않음. |
CitationDocument |
AnthropicCitationDocument로 이름 변경. |
AnthropicCacheType, StreamHelper, metadata.AnthropicRateLimit |
직접적인 대체 없이 제거. |
Default maxTokens |
500에서 4096으로 변경. |
Transitive com.squareup.okhttp3:okhttp |
새로 추가, com.anthropic:anthropic-java에 의해 전이됨. |
If You Use Only ChatClient or ChatModel
만약 코드가 이렇게 생겼다면, 마이그레이션이 필요 없어요:
@Autowired ChatClient.Builder builder;
String response = builder.build()
.prompt("Tell me a joke")
.call()
.content();
자동 설정은 새 SDK 기반 구현에 연결된 AnthropicChatModel 빈을 만들어요. 호출 코드는 영향을 받지 않아요.
주시해야 할 유일한 동작 변경은 새로운 기본 maxTokens 값이에요 (아래 Default maxTokens is now 4096 참고).
If You Construct AnthropicChatModel Programmatically
직접 생성자 사용을 빌더로 바꿔주세요:
// Before
AnthropicApi anthropicApi = new AnthropicApi(apiKey);
AnthropicChatModel chatModel = new AnthropicChatModel(anthropicApi,
AnthropicChatOptions.builder().model("claude-haiku-4-5").maxTokens(2048).build(),
retryTemplate,
toolCallingManager);
// After
AnthropicChatModel chatModel = AnthropicChatModel.builder()
.apiKey(apiKey)
.defaultOptions(AnthropicChatOptions.builder()
.model("claude-haiku-4-5")
.maxTokens(2048)
.build())
.toolCallingManager(toolCallingManager)
.build();
빌더는 baseUrl, timeout, maxRetries, proxy, customHeaders, observationRegistry, observationConvention도 받아요. retryTemplate 빌더 메서드는 없어요. 이제 재시도는 SDK가 처리해요 (아래 Retry uses SDK maxRetries, not RetryTemplate 참고).
If You Imported Cache or Citation Types
캐시와 인용 도우미 클래스가 api(및 api.utils) 하위 패키지에서 루트 org.springframework.ai.anthropic 패키지로 이동했어요. import를 다음과 같이 업데이트하세요:
| Old import | New import |
|---|---|
org.springframework.ai.anthropic.api.AnthropicCacheOptions |
org.springframework.ai.anthropic.AnthropicCacheOptions |
org.springframework.ai.anthropic.api.AnthropicCacheStrategy |
org.springframework.ai.anthropic.AnthropicCacheStrategy |
org.springframework.ai.anthropic.api.AnthropicCacheTtl |
org.springframework.ai.anthropic.AnthropicCacheTtl |
org.springframework.ai.anthropic.api.utils.CacheBreakpointTracker |
org.springframework.ai.anthropic.CacheBreakpointTracker |
org.springframework.ai.anthropic.api.utils.CacheEligibilityResolver |
org.springframework.ai.anthropic.CacheEligibilityResolver |
org.springframework.ai.anthropic.api.CitationDocument |
org.springframework.ai.anthropic.AnthropicCitationDocument |
AnthropicCacheStrategy(NONE, TOOLS_ONLY, SYSTEM_ONLY, SYSTEM_AND_TOOLS, CONVERSATION_HISTORY)와 AnthropicCacheTtl(FIVE_MINUTES, ONE_HOUR)의 Enum 값은 변경되지 않았어요. 이름이 바뀐 AnthropicCitationDocument의 plainText(…), pdf(…), customContent(…) 팩토리 메서드도 여전히 존재해요.
If You Used AnthropicApi Directly
AnthropicApi, 그 중첩 DTO record, AnthropicCacheType은 사라졌어요. SDK 클라이언트를 사용하기 전에, AnthropicChatModel이 하고 있던 일을 충당하는지 고려해보세요. 대부분의 경우 충당하며, 프레임워크 통합도 유지할 수 있어요.
AnthropicChatModel은 원시 AnthropicClient 위에 다음을 추가해요:
-
제공자 중립적 요청/응답 타입(
Prompt,ChatResponse,Generation,Usage) — 덕분에 애플리케이션 코드가com.anthropic.*에 의존하지 않아요. -
ToolCallback과ToolCallingManager루프와 통합된 툴 호출, 자동 멀티 턴 실행 포함. -
SDK의 콜백 기반
AsyncStreamResponse대신 ReactorFlux<ChatResponse>로의 스트리밍. -
전략 모델링, TTL 제어, 4-브레이크포인트 강제가 있는 프롬프트 캐싱(
AnthropicCacheOptions). -
ChatResponseMetadata아래에 표면화되는 네 가지 위치 변형이 있는Citation. -
AnthropicChatOptions필드로서의 스킬, 내장 웹 검색, 서비스 티어, inference geo, 구조화된 출력 — 모두spring.ai.anthropic.chat.*에서 바인딩 가능. -
Spring Boot 자동 설정과 Micrometer 관측.
-
모델 위의
ChatClient파이프라인(어드바이저, 메시지 템플릿, RAG, 대화 메모리, 구조화된 출력 컨버터). -
제공자 이식성: 동일한
ChatClient코드가 OpenAI, Bedrock, Google GenAI 등에서도 동작해요.
일반적인 채팅 사용이라면 AnthropicChatModel.builder()로 전환하세요 (위 If You Construct AnthropicChatModel Programmatically 참고):
// Before
AnthropicApi api = new AnthropicApi(apiKey);
AnthropicApi.ChatCompletionRequest req = new AnthropicApi.ChatCompletionRequest(
AnthropicApi.ChatModel.CLAUDE_HAIKU_4_5.getValue(),
List.of(new AnthropicApi.AnthropicMessage(List.of(new AnthropicApi.ContentBlock("hello")), AnthropicApi.Role.USER)),
null, 1024, null, 0.7, null, null, null, null, false);
ResponseEntity<AnthropicApi.ChatCompletionResponse> resp = api.chatCompletionEntity(req);
// After
AnthropicChatModel chatModel = AnthropicChatModel.builder()
.apiKey(apiKey)
.defaultOptions(AnthropicChatOptions.builder()
.model("claude-haiku-4-5")
.maxTokens(1024)
.temperature(0.7)
.build())
.build();
ChatResponse response = chatModel.call(new Prompt("hello"));
AnthropicChatModel이 노출하지 않는 Anthropic API 표면(베타 엔드포인트, files API, 커스텀 스킬 CRUD)이 정말 필요하다면, SDK 클라이언트로 내려가세요. 그 시점부터는 프레임워크 밖이에요 — 관측도, 제공자 중립성, ChatClient 파이프라인도 없어요:
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.models.messages.MessageCreateParams;
import com.anthropic.models.messages.Model;
AnthropicClient client = AnthropicOkHttpClient.builder().apiKey(apiKey).build();
MessageCreateParams params = MessageCreateParams.builder()
.model(Model.CLAUDE_HAIKU_4_5)
.maxTokens(1024)
.temperature(0.7)
.addUserMessage("hello")
.build();
com.anthropic.models.messages.Message message = client.messages().create(params);
수제로 만든 record 타입들은 SDK의 com.anthropic.models.messages.* 아래에 직접적인 대응물이 있어요:
Removed type (old AnthropicApi.*) |
SDK replacement (com.anthropic.models.messages.*) |
|---|---|
ChatCompletionRequest |
MessageCreateParams |
ChatCompletionResponse |
Message |
AnthropicMessage |
MessageParam |
ContentBlock |
ContentBlock (sealed union: TextBlock, ToolUseBlock, ThinkingBlock, RedactedThinkingBlock, ServerToolUseBlock, WebSearchToolResultBlock, ContainerUploadBlock, …) |
Tool |
Tool |
ToolChoiceAuto / ToolChoiceAny / ToolChoiceTool / ToolChoiceNone |
ToolChoice (sealed union, ToolChoiceAuto, ToolChoiceAny, ToolChoiceTool, ToolChoiceNone 변형 포함) |
Source (image / PDF media) |
Base64ImageSource, UrlImageSource, Base64PdfSource, UrlPdfSource |
MessageStartEvent, ContentBlockStartEvent, ContentBlockDeltaEvent, MessageDeltaEvent, … |
RawMessageStreamEvent (sealed union) |
| __ | ToolUseBlock.input()은 JSON 문자열이 아니라 SDK의 JsonValue를 반환해요. JsonValue에 .toString()을 호출하면 JSON처럼 보이지만 실제로는 아닌 Java 맵 문법({key=value})이 생성돼요. 실제 JSON 문자열을 얻으려면 JsonValue.Visitor<T>로 탐색하거나 Jackson을 통해 직렬화하세요. |
|---|
Removed in Favor of SDK Equivalents
이 타입들은 SDK가 이미 같은 개념을 노출하므로 제거됐어요. SDK 타입을 직접 사용하세요.
| Removed | Use instead |
|---|---|
org.springframework.ai.anthropic.api.AnthropicCacheType |
com.anthropic.models.messages.CacheControlEphemeral. 이전 enum은 ephemeral 리터럴 값 하나만 담고 있었는데, 이는 API가 지원하는 유일한 cache-control 타입이기도 해요. |
org.springframework.ai.anthropic.api.StreamHelper |
com.anthropic.core.http.AsyncStreamResponse<RawMessageStreamEvent>. StreamHelper는 이전 WebClient 스트리밍 경로를 위한 내부 SSE 병합 도우미였어요. 이제 SDK가 스트림 이벤트를 네이티브로 전달하고, Spring AI가 내부적으로 Reactor Flux로 연결해요. ChatModel.stream(Prompt)를 호출하는 코드는 영향받지 않아요. |
org.springframework.ai.anthropic.metadata.AnthropicRateLimit |
com.anthropic.errors.RateLimitException(재시도가 소진된 후 SDK가 던짐), 그리고 SDK 응답 객체의 헤더. ChatResponseMetadata에서 rate-limit 메타데이터를 읽던 코드를 예외 처리로 바꾸세요. |
Behavior Changes
Prompt-level options no longer merge with model defaults
이전 모듈은 ModelOptionsUtils.copyToTarget(…)와 ModelOptionsUtils.merge(…)를 통해 프롬프트 수준 AnthropicChatOptions를 모델 수준 기본값에 병합해서, model, temperature 및 다른 설정되지 않은 필드가 모델 기본값으로 채워졌어요:
// Before: only maxTokens set; model + temperature inherited from defaults.
Prompt prompt = new Prompt(
"Tell me a joke",
AnthropicChatOptions.builder().maxTokens(2048).build());
chatModel.call(prompt);
새 모듈은 병합하지 않아요. 프롬프트 수준 옵션 인스턴스는 그대로 사용되며, 프롬프트에 옵션이 없으면 모델 기본값이 사용돼요.
// After: prompt-level options must be "full", or null.
Prompt prompt = new Prompt(
"Tell me a joke",
chatModel.getOptions().mutate().maxTokens(2048).build());
chatModel.call(prompt);
이는 Upgrading to 2.0.0-M5 — ChatOptions Handling의 더 넓은 변경을 Anthropic에 대해 일찍 적용한 것입니다. ChatClient는 모델에 도달하기 전에 자체 병합을 수행하므로, ChatClient를 사용하는 호출자는 영향을 받지 않아요.
Default maxTokens is now 4096
AnthropicChatOptions는 maxTokens를 500 대신 4096으로 기본 설정해요. 이전 기본값은 응답을 흔히 잘랐어요(truncate). 새 값은 다른 Spring AI 채팅 모듈과 일치해요.
비용을 제한하기 위해 500에 의존했다면 명시적으로 설정하세요:
spring.ai.anthropic.chat.max-tokens=500
Retry uses SDK maxRetries, not RetryTemplate
이전 모듈은 생성자에서 Spring Retry RetryTemplate을 받았어요. 이제 재시도는 SDK가 처리하며 maxRetries(기본값 2)로 설정해요:
spring.ai.anthropic.max-retries=5
Anthropic 모듈에 특별히 연결된 RetryTemplate 빈은 제거할 수 있어요. 더 세밀한 제어가 필요하면 커스텀 AnthropicSetup으로 SDK 클라이언트를 구성하세요.
Streaming thinking events
원시 Flux<ChatResponse> 스트림을 구독하는 사람(ChatClient나 MessageAggregator가 접는 것을 두지 않고)에게 영향을 주는 두 가지 변경이 있어요.
이전 모듈은 thinking 블록의 텍스트와 서명을 단일 Generation으로 묶었어요. SDK는 이를 별도의 이벤트로 전달하고, 새 모듈도 그 방식으로 전달해요: properties.signature가 있는 Generation은 thinking 텍스트 델타 이후, 이후의 텍스트 델타 전에 도착해요. MessageAggregator와 ChatClient의 내장 집계는 추가 청크를 투명하게 흡수해요.
Thinking 텍스트 델타에는 이제 properties.thinking = Boolean.TRUE도 포함돼요. 이전 모듈은 이를 일반 콘텐츠로 내보내서, 스트림 중간에서 thinking 텍스트와 응답 텍스트를 구분할 신뢰할 수 있는 방법이 호출자에게 없었어요.
스트리밍 메타데이터 키의 전체 집합:
| Block | Generation carries |
|---|---|
| Thinking text delta | content = <thinking text>, properties.thinking = true (M3에서 새로 추가) |
| Thinking signature delta | 빈 content, properties.signature = <signature> |
| Redacted thinking block | 빈 content, properties.data = <data> |
| Sync thinking block (non-streaming) | content = <thinking text>, properties.signature = <signature> (단일 Generation) |
Prompt caching changes
AnthropicCacheOptions를 설정했다면 알아야 할 세 가지 동작이 있어요:
-
4-브레이크포인트 제한은 API가 아니라 Spring AI에서 강제됩니다. Anthropic은 요청당 최대 4개의 캐시 브레이크포인트를 허용해요. 이전 모듈은 마커를 통과시키고 API가 초과분을 거부하게 두었어요. 이제
CacheBreakpointTracker가 개수를 유지하고 4개를 넘는 추가는 조용히 건너뛰며, 한 번의WARN을 로깅해요. 한도에 도달할 가장 흔한 방법은 멀티 블록 시스템 캐싱과 인용 문서를 결합한SYSTEM_AND_TOOLS예요. 이전에는 API 오류로 실패하던 요청이 이제 캐싱을 줄여 성공할 수 있으므로, 업그레이드 후 캐시 적중률을 확인해보세요. -
AnthropicCacheStrategy.NONE은 "설정 안 함(unset)"이지 "비활성화(disabled)"가 아니에요. 프롬프트 수준에서NONE을 설정하면 모델 기본값으로 폴백해요. 모델 기본값이 켜져 있을 때 캐싱을 끄려면,AnthropicCacheOptions.disabled()를 기본값으로 하는 두 번째AnthropicChatModel을 만드세요. -
툴 캐시 TTL은
MessageType.SYSTEM에서 가져와요.resolveToolCacheControl은 전략과 무관하게messageTypeTtl(MessageType.SYSTEM)을 조회해요.messageTypeTtl(MessageType.USER, ONE_HOUR)를 설정해도 툴 캐싱에는 효과가 없어요.
Citation document consistency is validated client-side
Anthropic API는 단일 요청에서 인용 구성을 가진 모든 DocumentBlockParam이 동일한 citations.enabled 값을 공유하도록 요구해요. 이제 AnthropicChatOptions.validateCitationConsistency()가 이를 강제하고, 요청을 보내기 전에 IllegalArgumentException을 던져요. 이전 모듈은 API가 HTTP 400을 반환하도록 두었어요. 활성화된 인용 문서와 비활성화된 인용 문서를 섞은 테스트나 호출 지점은 이제 네트워크 호출 대신 빌드 시점에 실패해요.
New OkHttp Transitive Dependency
com.anthropic:anthropic-java는 com.squareup.okhttp3:okhttp를 끌어와요. 대부분의 애플리케이션은 눈치채지 못할 거예요. 엄격한 의존성 수렴 규칙이 있거나 기존 OkHttp 고정 버전이 있다면 <dependencyManagement> 항목이 필요할 수 있어요.
New Capabilities
이 마이그레이션은 또한 여러 Anthropic 기능을 활성화해요. 전체 참조는 Anthropic Chat을 참고하세요.
-
네이티브 스킬 (
AnthropicSkill,AnthropicSkillContainer). -
내장 웹 검색 툴 (
AnthropicWebSearchTool). -
서비스 티어 선택 (
AnthropicServiceTier). -
데이터 상주를 위한 inference geo (
us,eu). -
JsonOutputFormat과Effort를 통한 네이티브 구조화된 출력 (claude-sonnet-4-6이상 필요). -
확장된 사고 표시 모드 (summarized / omitted).
-
네 가지 위치 변형(
CHAR_LOCATION,PAGE_LOCATION,CONTENT_BLOCK_LOCATION,WEB_SEARCH_RESULT_LOCATION)이 있는Citation타입. -
AbstractAnthropicOptions의 클라이언트 수준customHeaders와 구별되는,AnthropicChatOptions#httpHeaders의 요청별 HTTP 헤더.customHeaders는 클라이언트에서 한 번 설정되고 모든 요청에 적용돼요.httpHeaders는Prompt별로 설정되고 요청 빌드 시점에 병합돼요. 요청 추적, 베타 API 토글, 라우팅에 유용해요.
Things That Fail Silently
컴파일 오류는 쉽게 잡아요. 이것들은 던지지 않고, 대신 다른 출력을 만들어내요.
-
부분적인 프롬프트 수준 옵션.
new Prompt(text, AnthropicChatOptions.builder().maxTokens(2048).build())는 더 이상 모델의 기본값에서model,temperature등을 상속하지 않아요 (위 Prompt-level options no longer merge with model defaults 참고). 컴파일 오류도, 예외도 없어요. 요청이 다른 값으로 실행될 뿐이에요. 업그레이드 후 출력이 달라지고ChatModel을 직접 호출한다면, 가장 먼저 이 부분을 살펴보세요. -
업그레이드 후 비용이 오르면, 응답을 제한하기 위해 이전
500-토큰 기본maxTokens에 의존하고 있었는지 확인해보세요. -
4개를 넘는 캐시 브레이크포인트는 버려집니다. 멀티 블록 시스템 캐싱을 툴과 인용 문서와 함께 쌓으면 Anthropic의 4-브레이크포인트 한도를 넘을 수 있어요.
CacheBreakpointTracker는 초과분을 건너뛰고 한 번의WARN을 로깅해요. 오류 없이 캐시 적중률이 떨어져요. -
낡은
org.springframework.ai.anthropic.api.AnthropicCache*import는 업그레이드 후 대부분의 컴파일 오류의 원인이에요. 프로젝트 전체 찾기-바꾸기로 고칠 수 있어요.