Spring AI를 발견하기 전까지 AI 통합에 3일을 허비했습니다 — 저의 전체 Spring Boot 챗봇 구축 과정
요약
Spring AI 2.0을 사용하여 Spring Boot 기반의 AI 챗봇을 구축하는 전체 과정을 다룹니다. OpenAI, Anthropic 등 다양한 LLM을 통합하고 RAG를 구현하는 효율적인 방법을 제시합니다.
핵심 포인트
- Spring AI를 통한 LLM 통합 추상화 및 boilerplate 코드 감소
- OpenAI, Anthropic, Ollama 등 다양한 모델 지원
- 벡터 스토어 통합을 통한 RAG(검색 증강 생성) 구현
- Java 21 및 Spring Boot 3.4 환경에서의 실전 구축 가이드
6개월 전, 저는 OpenAI API를 호출하기 위해 RestTemplate 호출을 수동으로 이어 붙이고, 직접 재시도 로직 (retry logic)을 작성하며, 가공되지 않은 JSON 응답을 파싱하면서, 왜 아무도 이 과정을 덜 고통스럽게 만들지 않았는지 의아해하던 사람이었습니다.
그러다 Spring AI 2.0이 출시되었고, 저는 기다리지 않은 제 자신이 정말 바보같이 느껴졌습니다.
이것은 제가 빈 Spring Boot 프로젝트부터 배포 가능한 프로덕션 준비 단계의 AI 어시스턴트까지 직접 구축한 바로 그 챗봇입니다. 군더더기나 "남은 연습은 독자에게 맡깁니다" 같은 내용은 없습니다. 실제 코드, 실제 실수, 그리고 실제 주의 사항(gotchas)이 담겨 있습니다.
Spring AI란 무엇이며 2026년에 왜 중요한가?
Spring AI는 AI 모델과 작업하기 위한 Spring의 공식 추상화 계층 (abstraction layer)입니다. Spring Data가 JDBC의 상용구 코드 (boilerplate)를 추상화했던 것과 마찬가지로, Spring AI는 LLM 통합을 위해 동일한 역할을 수행한다고 생각하면 됩니다.
2.0 버전 (2026년 초 출시)에서 도입된 기능은 다음과 같습니다:
- OpenAI, Anthropic, Ollama, Azure OpenAI 등을 아우르는 통합
ChatClientAPI - 내장된 스트리밍 (streaming) 지원
- 벡터 스토어 (vector store) 통합을 통한 일급 RAG (Retrieval-Augmented Generation, 검색 증강 생성) 지원
- 프롬프트 (prompt)를 가로채고 보강하기 위한 Advisors API
Java 개발자들에게 이것은 엄청난 변화입니다. 더 이상 Python 프레임워크를 배울 필요도, 취약한 HTTP 래퍼 (wrapper) 클래스를 유지 관리할 필요도 없습니다. 이것이 진정한, 관용적인 (idiomatic) Spring입니다.
프로젝트 설정
저는 Java 21과 Spring Boot 3.4.x를 사용했습니다. 필요한 핵심 pom.xml 섹션은 다음과 같습니다:
<properties>
<java.version>21</java.version>
<spring-ai.version>2.0.0</spring-ai.version>
...
application.properties에 API 키를 추가하세요:
spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.options.model=gpt-4o
spring.ai.openai.chat.options.temperature=0.7
...
API 키를 절대 하드코딩하지 마세요. 당연한 소리처럼 들리겠지만, 저는 올해 3개의 서로 다른 GitHub 저장소에서 이를 목격했습니다.
아키텍처 개요
코드로 들어가기 전에, 우리가 구축할 것의 형태는 다음과 같습니다:
┌─────────────────────────────────────────────────┐
│ CLIENT (Browser/App) │
└───────────────────┬─────────────────────────────┘
...
단순하고, 깔끔하며, 교체 가능합니다. 만약 내일 당장 Ollama를 로컬에서 실행하고 싶다면, 의존성(dependency) 하나와 설정(properties) 한 줄만 바꾸면 됩니다.
챗봇 구축하기 — 단계별 가이드
서비스 레이어 (The Service Layer)
여기가 실제 로직이 살아 숨 쉬는 곳입니다. 저는 컨트롤러(controller)를 가볍게 유지하고, 의미 있는 모든 로직을 여기에 담았습니다.
@Service
@Slf4j
public class ChatService {
...
ChatClient.Builder는 Spring AI에 의해 자동 설정(auto-configured)되므로, 단순히 주입(inject)하기만 하면 됩니다. defaultSystem() 호출은 모든 대화에 대해 지속적인 시스템 프롬프트(system prompt)를 설정합니다. 이것이 봇의 성격을 정의하는 설정입니다.
컨트롤러 (The Controller)
@RestController
@RequestMapping("/api/chat")
@Validated
...
DTO를 위한 레코드(Records). 입력값에 대한 검증(Validation) 어노테이션. 보일러플레이트(boilerplate) 코드도 없습니다. 이것이 Java 21과 Spring Boot 3가 제대로 작동할 때 느껴지는 경험입니다.
대화 메모리 추가하기
위의 방식은 상태가 없는(stateless) 방식입니다. 즉, 모든 호출이 새로운 컨텍스트(context)로 처리됩니다. 실제 챗봇을 만들려면 메모리가 필요합니다. Spring AI는 MessageChatMemoryAdvisor를 통해 이를 처리합니다.
@Service
@Slf4j
public class StatefulChatService {
...
conversationId(세션 ID나 사용자 ID와 같은)를 전달하면, Spring AI가 해당 대화에 대한 메시지 기록을 자동으로 유지합니다. 운영 환경(production)에서는 InMemoryChatMemory를 Redis 또는 데이터베이스 기반의 구현체로 교체하세요.
운영 환경 고려 사항
이 부분은 대부분의 튜토리얼이 건너뛰는 내용입니다. 스테이징(staging) 환경에서 저를 괴롭혔던 부분들을 소개합니다.
속도 제한 (Rate Limiting)
OpenAI에는 TPM(tokens per minute, 분당 토큰 수) 제한이 있습니다. 부하가 걸리면 이 제한에 걸리게 됩니다. 간단한 토큰 버킷(token bucket) 알고리즘을 추가하거나 Resilience4j를 사용하세요.
@Bean
public RateLimiter aiRateLimiter(RateLimiterRegistry registry) {
return registry.rateLimiter("openai", RateLimiterConfig.custom()
...
chatService.chat() 호출을 RateLimiter.decorateSupplier(...)로 감싸세요.
타임아웃 (Timeouts)
OpenAI의 API는 느려질 수 있습니다. 특히 부하가 높을 때 더욱 그렇습니다. 명시적인 타임아웃을 설정하세요.
spring.ai.openai.chat.options.timeout=30s
그리고 TimeoutException을 다른 오류와 별도로 처리하여, 일반적인 500 에러 대신 "AI 응답이 너무 오래 걸리고 있습니다. 다시 시도해 주세요"와 같이 사용자 친화적인 메시지를 반환할 수 있도록 하세요.
토큰 사용량 로깅 (Logging Token Usage)
비용을 추적해야 합니다. Spring AI의 응답 메타데이터 (response metadata)를 사용하세요:
ChatResponse response = chatClient.prompt()
.user(userMessage)
.call()
...
이러한 지표 (metrics)를 관측성 스택 (observability stack)으로 전송하세요. 추적되지 않은 AI 비용은 결제 시점에 당신을 놀라게 할 것입니다.
💡 프로 팁 (Pro Tip): Spring AI의
Advisor패턴을 사용하여 서비스 레이어 (service layer)를 오염시키지 않고도 콘텐츠 필터링 (content filtering), 개인정보(PII) 탐지, 감사 로깅 (audit logging)과 같은 횡단 관심사 (cross-cutting concerns)를 구현하세요. Advisor를 AI 호출을 위한 AOP라고 생각하면 됩니다.
배포 팁 (Deployment Tips)
저는 이것을 기본적인 OpenShift 컨테이너(저희 회사의 표준입니다)에 배포했습니다. 주목할 만한 몇 가지 사항은 다음과 같습니다:
프로퍼티 파일 (properties files)보다 환경 변수 (Environment variables)를 사용하세요. OPENAI_API_KEY를 시크릿 (secret)으로 설정하고, 프로퍼티에서 ${OPENAI_API_KEY}로 참조하세요. 절대로 키를 이미지에 직접 포함(bake)하지 마세요.
헬스 체크 (Health checks)가 중요합니다. 시작 시 AI 제공업체에 핑 (ping)을 보내는 커스텀 헬스 인디케이터 (health indicator)를 추가하세요:
@Component
public class AiHealthIndicator implements HealthIndicator {
private final ChatClient chatClient;
...
수평 확장 (Horizontal scaling)은 괜찮습니다 — AI 상태 (state)는 LLM 제공업체에 존재하므로, 애플리케이션은 무상태 (stateless)입니다 (단, 인메모리 채팅 기록을 사용하는 경우는 예외이며, 이 경우 세션을 고정하거나 Redis를 사용해야 합니다).
제가 했던 실수들 (여러분은 하지 마세요)
1. max-tokens 설정을 잊어버린 것.
제한이 없으면 모델은 완료될 때까지 계속 생성할 것입니다. 긴 응답 = 높은 비용 = 느린 응답입니다. 항상 max-tokens를 설정하세요.
2. 시스템 프롬프트 (system prompt)에 너무 많은 내용을 넣은 것.
저는 500단어 분량의 시스템 프롬프트에 비즈니스 로직을 쑤셔 넣었습니다. 이로 인해 출력이 일관되지 않았습니다. 시스템 프롬프트는 집중력 있게 짧게 유지하세요 — 이상적으로는 200단어 미만이 좋습니다.
3. 빈 응답 또는 null 응답을 처리하지 않은 것.
모델이 "stop" 이외의 중단 사유(예: 콘텐츠 필터링)를 반환하는 경우, Spring AI의 .content()는 null을 반환할 수 있습니다. null 체크를 추가하세요.
4. 통합 테스트 (Integration tests) 건너뛰기.
ChatClient를 모킹(mocked)한 단위 테스트 (Unit tests)는 괜찮지만, 실제 API를 호출하는 통합 테스트가 최소 몇 개는 필요합니다. 모델의 동작은 버전 간에 변경될 수 있습니다. 저는 이 방식을 통해 프로덕션에 반영되기 전에 회귀 오류 (regression)를 발견할 수 있었습니다.
5. WebFlux 컨텍스트에서 블로킹 호출 (Blocking calls) 사용하기.
Spring AI의 블로킹 call() API를 리액티브 (reactive) 코드와 혼용하면 스레드 기아 (thread starvation) 현상이 발생합니다. 완전히 리액티브하게 가거나 ( .stream()을 사용하고 Flux<String>을 반환), 아니면 전용 스레드 풀 (dedicated thread pool)에서 실행하세요.
핵심 요약 (Key Takeaways)
- Spring AI 2.0을 사용하면 LLM 통합이 다른 Spring 서비스 작성과 마찬가지로 관용적 (idiomatic)이고, 테스트 가능하며, 제공자 불가지론적 (provider-agnostic)인 느낌을 줍니다.
ChatClient빌더 패턴 (builder pattern)이 주요 API 접점입니다. 이를 잘 익혀두세요.- 메모리 관리 (
MessageChatMemoryAdvisor) 기능이 내장되어 있습니다. 직접 구현 (hand-roll)하지 마세요. - 프로덕션 준비 상태 (Production readiness)란 다음을 의미합니다: 속도 제한 (rate limiting), 타임아웃 (timeouts), 토큰 추적 (token tracking), 그리고 적절한 상태 확인 (health checks).
- Advisors API는 과소평가되어 있습니다. 횡단 관심사 (cross-cutting concerns)인 AI 관련 처리에 이를 활용하세요.
다음 단계 (What's Next)
현재 이 프로젝트에 RAG (검색 증강 생성)를 추가하고 있습니다. 내부 문서를 벡터 저장소 (PostgreSQL의 PGVector)에 임베딩 (embedding)하여, 챗봇이 환각 (hallucination)을 일으키는 대신 실제 문서에 근거하여 질문에 답변할 수 있도록 하고 있습니다.
그 이후에는: UI가 전체 완료를 기다리는 대신 즉각적으로 느껴지도록 SSE를 이용한 스트리밍 응답 (streaming responses)을 구현할 예정입니다.
이 내용이 도움이 되었다면, 여기 DEV에서 저를 팔로우해 주세요. 다음 주에 RAG 후속 포스팅을 올리겠습니다.
Spring Boot 프로젝트에 AI를 통합해 보셨나요? 가장 어려웠던 점은 무엇이었나요 — 설정, 프롬프팅 (prompting), 아니면 팀원들에게 프로덕션 환경에서 안전하다는 것을 설득하는 것이었나요? 댓글로 남겨주세요. 👇
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기